diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..028bbab --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,141 @@ +/* eslint-disable max-lines-per-function */ +/* eslint-disable no-console */ +import React, { FC, useEffect } from 'react' +import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import OpenAPIParser from '@readme/openapi-parser' +import { OpenAPIV2 } from 'openapi-types' +import { ConfigProvider, theme as antdtheme } from 'antd' +import { getSwagger } from '@prorobotech/openapi-k8s-toolkit' +import { useSelector, useDispatch } from 'react-redux' +import type { RootState } from 'store/store' +import { setSwagger } from 'store/swagger/swagger/swagger' +import { setIsFederation } from 'store/federation/federation/federation' +import { setBaseprefix } from 'store/federation/federation/baseprefix' +import { + MainPage, + ClusterAndNsListPage, + ListApiPage, + ListCrdByApiGroupPage, + ListApiByApiGroupPage, + TableCrdPage, + TableApiPage, + TableBuiltinPage, + FormBuiltinPage, + FormApiPage, + FormCrdPage, + FactoryPage, +} from 'pages' +import { getBasePrefix } from 'utils/getBaseprefix' +import { colorsLight, colorsDark, sizes } from 'constants/colors' + +type TAppProps = { + isFederation?: boolean + forcedTheme?: 'dark' | 'light' +} + +const queryClient = new QueryClient() + +export const App: FC = ({ isFederation, forcedTheme }) => { + const dispatch = useDispatch() + const theme = useSelector((state: RootState) => state.openapiTheme.theme) + const cluster = useSelector((state: RootState) => state.cluster.cluster) + + const basePrefix = getBasePrefix(isFederation) + + useEffect(() => { + if (isFederation) { + dispatch(setIsFederation(true)) + } + const basePrefix = getBasePrefix(isFederation) + dispatch(setBaseprefix(basePrefix)) + }, [dispatch, isFederation]) + + useEffect(() => { + getSwagger({ clusterName: cluster }) + .then(({ data }) => { + OpenAPIParser.dereference(data, { + dereference: { + circular: 'ignore', + }, + }) + .then(data => { + // deference is a cruel thing + dispatch(setSwagger(data as OpenAPIV2.Document)) + }) + .catch(error => { + console.log('Swagger: deref error', error) + }) + }) + .catch(error => { + console.log('Swagger: fetch error', error) + }) + }, [cluster, dispatch]) + + const renderRoutes = (prefix = '') => ( + + } /> + } /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } /> + + ) + + const colors = theme === 'dark' ? colorsDark : colorsLight + + return ( + + {import.meta.env.MODE === 'development' && } + + {isFederation ? renderRoutes() : {renderRoutes(basePrefix)}} + + + ) +} diff --git a/src/components/atoms/DefaultColorProvider/DefaultColorProvider.ts b/src/components/atoms/DefaultColorProvider/DefaultColorProvider.ts new file mode 100644 index 0000000..4fe75c5 --- /dev/null +++ b/src/components/atoms/DefaultColorProvider/DefaultColorProvider.ts @@ -0,0 +1,13 @@ +import styled from 'styled-components' + +type TDefaultColorProviderProps = { + $color: string +} + +export const DefaultColorProvider = styled.div` + color: ${({ $color }) => $color}; + + td { + color: ${({ $color }) => $color}; + } +` diff --git a/src/components/atoms/DefaultColorProvider/index.ts b/src/components/atoms/DefaultColorProvider/index.ts new file mode 100644 index 0000000..156ba90 --- /dev/null +++ b/src/components/atoms/DefaultColorProvider/index.ts @@ -0,0 +1 @@ +export * from './DefaultColorProvider' diff --git a/src/components/atoms/DefaultLayout/DefaultLayout.ts b/src/components/atoms/DefaultLayout/DefaultLayout.ts new file mode 100644 index 0000000..eb91025 --- /dev/null +++ b/src/components/atoms/DefaultLayout/DefaultLayout.ts @@ -0,0 +1,31 @@ +import styled from 'styled-components' + +type TLayoutProps = { + $bgColor: string +} + +const Layout = styled.div` + background: ${({ $bgColor }) => $bgColor}; + min-height: 100vh; + width: 100%; +` + +const ContentContainer = styled.div` + min-height: 100vh; + margin: 0; +` + +type TContentPaddingProps = { + $isFederation?: boolean +} + +const ContentPadding = styled.div` + padding: ${({ $isFederation }) => ($isFederation ? 0 : '24px')}; + min-height: 100vh; +` + +export const DefaultLayout = { + Layout, + ContentContainer, + ContentPadding, +} diff --git a/src/components/atoms/DefaultLayout/index.ts b/src/components/atoms/DefaultLayout/index.ts new file mode 100644 index 0000000..f39ae0f --- /dev/null +++ b/src/components/atoms/DefaultLayout/index.ts @@ -0,0 +1 @@ +export * from './DefaultLayout' diff --git a/src/components/atoms/ThemeSelector/ThemeSelector.tsx b/src/components/atoms/ThemeSelector/ThemeSelector.tsx new file mode 100644 index 0000000..f756ef1 --- /dev/null +++ b/src/components/atoms/ThemeSelector/ThemeSelector.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react' +import { Switch } from 'antd' +import { useSelector, useDispatch } from 'react-redux' +import type { RootState } from 'store/store' +import { setTheme } from 'store/theme/theme/theme' +import { Styled } from './styled' + +export const ThemeSelector: FC = () => { + const dispatch = useDispatch() + const theme = useSelector((state: RootState) => state.openapiTheme.theme) + + const updateTheme = (checked: boolean) => { + if (checked) { + localStorage.setItem('theme', 'dark') + dispatch(setTheme('dark')) + } else { + localStorage.setItem('theme', 'light') + dispatch(setTheme('light')) + } + } + + return ( + + Dark Mode + updateTheme(checked)} /> + + ) +} diff --git a/src/components/atoms/ThemeSelector/index.ts b/src/components/atoms/ThemeSelector/index.ts new file mode 100644 index 0000000..4ecada5 --- /dev/null +++ b/src/components/atoms/ThemeSelector/index.ts @@ -0,0 +1 @@ +export * from './ThemeSelector' diff --git a/src/components/atoms/ThemeSelector/styled.ts b/src/components/atoms/ThemeSelector/styled.ts new file mode 100644 index 0000000..df1df0d --- /dev/null +++ b/src/components/atoms/ThemeSelector/styled.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components' + +const Container = styled.div` + display: grid; + grid-column-gap: 4px; + grid-template-columns: repeat(2, auto); +` + +export const Styled = { + Container, +} diff --git a/src/components/atoms/TitleWithNoTopMargin/TitleWithNoTopMargin.tsx b/src/components/atoms/TitleWithNoTopMargin/TitleWithNoTopMargin.tsx new file mode 100644 index 0000000..8711332 --- /dev/null +++ b/src/components/atoms/TitleWithNoTopMargin/TitleWithNoTopMargin.tsx @@ -0,0 +1,6 @@ +import styled from 'styled-components' +import { Typography } from 'antd' + +export const TitleWithNoTopMargin = styled(Typography.Title)` + margin-top: 0; +` diff --git a/src/components/atoms/TitleWithNoTopMargin/index.tsx b/src/components/atoms/TitleWithNoTopMargin/index.tsx new file mode 100644 index 0000000..2e006a5 --- /dev/null +++ b/src/components/atoms/TitleWithNoTopMargin/index.tsx @@ -0,0 +1 @@ +export * from './TitleWithNoTopMargin' diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts new file mode 100644 index 0000000..5975c0d --- /dev/null +++ b/src/components/atoms/index.ts @@ -0,0 +1,4 @@ +export * from './DefaultColorProvider' +export * from './DefaultLayout' +export * from './TitleWithNoTopMargin' +export * from './ThemeSelector' diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..266a86f --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export * from './atoms' diff --git a/src/constants/basePrefix.ts b/src/constants/basePrefix.ts new file mode 100644 index 0000000..5e8feb1 --- /dev/null +++ b/src/constants/basePrefix.ts @@ -0,0 +1,4 @@ +import { getBasePrefix } from 'utils/getBaseprefix' + +// eslint-disable-next-line no-underscore-dangle +export const BASEPREFIX: string = getBasePrefix() diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..0432243 --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,214 @@ +import { ThemeConfig } from 'antd' + +const percentageToHex = (percentage: number) => { + const decimal = `0${Math.round(255 * (percentage / 100)).toString(16)}`.slice(-2).toUpperCase() + return decimal +} + +export const colorsLight: ThemeConfig['token'] = { + colorPrimaryBg: '#F0F5FF', + colorPrimaryBgHover: '#DBEAFE', + colorPrimaryBorder: '#BFDBFE', + colorPrimaryBorderHover: '#93C5FD', + colorPrimaryHover: '#60A5FA', + colorPrimary: '#3B82F6', + colorPrimaryActive: '#2563EB', + colorPrimaryTextHover: '#60A5FA', + colorPrimaryText: '#3B82F6', + colorPrimaryTextActive: '#2563EB', + colorLinkActive: '#2563EB', + colorLinkHover: '#60A5FA', + colorBgContainer: '#FFFFFF', + colorBgElevated: '#FFFFFF', + colorBgLayout: '#F1F5F9', + colorBgSpotlight: `#000000${percentageToHex(88)}`, + colorBgMask: `#000000${percentageToHex(45)}`, + colorBgContainerDisabled: `#000000${percentageToHex(4)}`, + controlItemBgHover: `#000000${percentageToHex(4)}`, + controlItemBgActive: '#F0F5FF', + controlItemBgActiveHover: '#DBEAFE', + controlItemBgActiveDisabled: `#000000${percentageToHex(15)}`, + colorBgTextActive: '#2563EB', + colorBgTextHover: '#60A5FA', + colorFill: `#000000${percentageToHex(15)}`, + colorFillSecondary: `#000000${percentageToHex(6)}`, + colorFillTertiary: `#000000${percentageToHex(4)}`, + colorFillQuaternary: `#000000${percentageToHex(2)}`, + colorText: `#000000${percentageToHex(88)}`, + colorTextLabel: `#000000${percentageToHex(65)}`, + colorTextDescription: `#000000${percentageToHex(45)}`, + colorTextPlaceholder: `#000000${percentageToHex(25)}`, + colorTextDisabled: `#000000${percentageToHex(25)}`, + colorIcon: `#000000${percentageToHex(45)}`, + colorIconHover: `#000000${percentageToHex(88)}`, + // colorTextSolid: '#FFFFFF', + colorBorder: '#CBD5E1', + colorBorderSecondary: '#E2E8F0', + colorSplit: `#000000${percentageToHex(6)}`, + colorSuccessBg: '#F6FFED', + colorSuccessBgHover: '#D9F7BE', + colorSuccessBorder: '#B7EB8F', + colorSuccessBorderHover: '#95DE64', + colorSuccessHover: '#73D13D', + colorSuccess: '#059669', + colorSuccessActive: '#389E0D', + colorSuccessTextHover: '#73D13D', + colorSuccessText: '#059669', + colorSuccessTextActive: '#389E0D', + colorWarningBg: '#FFFBE6', + colorWarningBgHover: '#FFF1B8', + colorWarningBorder: '#FFE58F', + colorWarningBorderHover: '#FFD666', + colorWarningHover: '#FFC53D', + colorWarning: '#CA8A04', + colorWarningActive: '#D48806', + colorWarningTextHover: '#FFC53D', + colorWarningText: '#CA8A04', + colorWarningTextActive: '#D48806', + colorErrorBg: '#FFF1F0', + colorErrorBgHover: '#FFCCC7', + colorErrorBorder: '#FFA39E', + colorErrorBorderHover: '#FF7875', + colorErrorHover: '#FF4D4F', + colorError: '#DC2626', + colorErrorActive: '#CF1322', + colorErrorTextHover: '#FF4D4F', + colorErrorText: '#DC2626', + colorErrorTextActive: '#CF1322', + colorInfoBg: '#F0F5FF', + colorInfoBgHover: '#DBEAFE', + colorInfoBorder: '#BFDBFE', + colorInfoBorderHover: '#93C5FD', + colorInfoHover: '#60A5FA', + colorInfo: '#3B82F6', + colorInfoActive: '#2563EB', + colorInfoTextHover: '#60A5FA', + colorInfoText: '#3B82F6', + colorInfoTextActive: '#2563EB', +} + +export const colorsDark: ThemeConfig['token'] = { + colorPrimaryBg: '#40485A', + colorPrimaryBgHover: '#2F3846', + colorPrimaryBorder: '#334155', + colorPrimaryBorderHover: '#475569', + colorPrimaryHover: '#64748B', + colorPrimary: '#818CF8', + colorPrimaryActive: '#A5B4FC', + colorPrimaryTextHover: '#64748B', + colorPrimaryText: '#818CF8', + colorPrimaryTextActive: '#A5B4FC', + colorLinkActive: '#A5B4FC', + colorLinkHover: '#64748B', + colorBgContainer: '#2D2F38', + colorBgElevated: '#2D2F38', + colorBgLayout: '#1E1F22', + colorBgSpotlight: `#FFFFFF${percentageToHex(88)}`, + colorBgMask: `#FFFFFF${percentageToHex(45)}`, + colorBgContainerDisabled: `#FFFFFF${percentageToHex(4)}`, + controlItemBgHover: `#FFFFFF${percentageToHex(4)}`, + controlItemBgActive: '#40485A', + controlItemBgActiveHover: '#2F3846', + controlItemBgActiveDisabled: `#FFFFFF${percentageToHex(15)}`, + colorBgTextActive: '#A5B4FC', + colorBgTextHover: '#64748B', + colorFill: `#FFFFFF${percentageToHex(15)}`, + colorFillSecondary: `#FFFFFF${percentageToHex(6)}`, + colorFillTertiary: `#FFFFFF${percentageToHex(4)}`, + colorFillQuaternary: `#FFFFFF${percentageToHex(2)}`, + colorText: `#FFFFFF${percentageToHex(88)}`, + colorTextLabel: `#FFFFFF${percentageToHex(65)}`, + colorTextDescription: `#FFFFFF${percentageToHex(45)}`, + colorTextPlaceholder: `#FFFFFF${percentageToHex(25)}`, + colorTextDisabled: `#FFFFFF${percentageToHex(25)}`, + colorIcon: `#FFFFFF${percentageToHex(45)}`, + colorIconHover: `#FFFFFF${percentageToHex(88)}`, + // colorTextSolid: '#FFFFFF', + colorBorder: '#4B5563', + colorBorderSecondary: '#374151', + colorSplit: `#FFFFFF${percentageToHex(5)}`, + colorSuccessBg: '#162312', + colorSuccessBgHover: '#1D3712', + colorSuccessBorder: '#274916', + colorSuccessBorderHover: '#306317', + colorSuccessHover: '#3C8618', + colorSuccess: '#34D399', + colorSuccessActive: '#6ABE39', + colorSuccessTextHover: '#3C8618', + colorSuccessText: '#34D399', + colorSuccessTextActive: '#6ABE39', + colorWarningBg: '#2B2111', + colorWarningBgHover: '#443111', + colorWarningBorder: '#594214', + colorWarningBorderHover: '#7C5914', + colorWarningHover: '#AA7714', + colorWarning: '#FACC15', + colorWarningActive: '#E8B339', + colorWarningTextHover: '#AA7714', + colorWarningText: '#FACC15', + colorWarningTextActive: '#E8B339', + colorErrorBg: '#2A1215', + colorErrorBgHover: '#431418', + colorErrorBorder: '#58181C', + colorErrorBorderHover: '#791A1F', + colorErrorHover: '#A61D24', + colorError: '#F87171', + colorErrorActive: '#E84749', + colorErrorTextHover: '#A61D24', + colorErrorText: '#F87171', + colorErrorTextActive: '#E84749', + colorInfoBg: '#40485A', + colorInfoBgHover: '#2F3846', + colorInfoBorder: '#334155', + colorInfoBorderHover: '#475569', + colorInfoHover: '#64748B', + colorInfo: '#818CF8', + colorInfoActive: '#A5B4FC', + colorInfoTextHover: '#64748B', + colorInfoText: '#818CF8', + colorInfoTextActive: '#A5B4FC', +} + +export const sizes: ThemeConfig['token'] = { + sizeXXL: 48, + sizeXL: 32, + sizeLG: 24, + sizeMD: 20, + size: 16, + sizeSM: 12, + sizeXS: 8, + sizeXXS: 4, + // paddingXXL: 48, + paddingXL: 32, + paddingLG: 24, + paddingMD: 20, + padding: 16, + paddingSM: 12, + paddingXS: 8, + paddingXXS: 4, + controlHeightLG: 40, + controlHeight: 32, + controlHeightSM: 24, + controlHeightXS: 16, + screenXXL: 1600, + screenXXLMin: 1600, + screenXL: 1200, + screenXLMax: 1599, + screenXLMin: 1200, + screenLG: 992, + screenLGMax: 1199, + screenLGMin: 992, + screenMD: 768, + screenMDMax: 991, + screenMDMin: 768, + screenSM: 576, + screenSMMax: 767, + screenSMMin: 576, + screenXS: 480, + screenXSMax: 575, + screenXSMin: 480, + borderRadiusLG: 8, + borderRadius: 6, + borderRadiusSM: 4, + borderRadiusXS: 4, +} diff --git a/src/federation.tsx b/src/federation.tsx new file mode 100644 index 0000000..f98931e --- /dev/null +++ b/src/federation.tsx @@ -0,0 +1,17 @@ +import React, { FC } from 'react' +import { Provider } from 'react-redux' +import { store } from 'store/store' +import { App } from './App' + +type TFederationAppProps = { + forcedTheme?: 'dark' | 'light' +} + +const FederationApp: FC = ({ forcedTheme }) => ( + + + +) + +// eslint-disable-next-line import/no-default-export +export default FederationApp diff --git a/src/store/cluster/cluster/cluster.ts b/src/store/cluster/cluster/cluster.ts new file mode 100644 index 0000000..88fe469 --- /dev/null +++ b/src/store/cluster/cluster/cluster.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +export type TState = { + cluster: string +} + +const initialState: TState = { + cluster: '', +} + +export const clusterSlice = createSlice({ + name: 'cluster', + initialState, + reducers: { + setCluster: (state, action: PayloadAction) => { + state.cluster = action.payload + }, + }, +}) + +export const { setCluster } = clusterSlice.actions diff --git a/src/store/clusterList/clusterList/clusterList.ts b/src/store/clusterList/clusterList/clusterList.ts new file mode 100644 index 0000000..445e164 --- /dev/null +++ b/src/store/clusterList/clusterList/clusterList.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { TClusterList } from '@prorobotech/openapi-k8s-toolkit' + +export type TState = { + clusterList: TClusterList | undefined +} + +const initialState: TState = { + clusterList: undefined, +} + +export const clusterListSlice = createSlice({ + name: 'clusterList', + initialState, + reducers: { + setClusterList: (state, action: PayloadAction) => { + state.clusterList = action.payload + }, + }, +}) + +export const { setClusterList } = clusterListSlice.actions diff --git a/src/store/federation/federation/baseprefix.ts b/src/store/federation/federation/baseprefix.ts new file mode 100644 index 0000000..8829984 --- /dev/null +++ b/src/store/federation/federation/baseprefix.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +export type TState = { + baseprefix?: string +} + +const initialState: TState = { + baseprefix: undefined, +} + +export const baseprefixSlice = createSlice({ + name: 'baseprefix', + initialState, + reducers: { + setBaseprefix: (state, action: PayloadAction) => { + state.baseprefix = action.payload + }, + }, +}) + +export const { setBaseprefix } = baseprefixSlice.actions diff --git a/src/store/federation/federation/federation.ts b/src/store/federation/federation/federation.ts new file mode 100644 index 0000000..15b4d6b --- /dev/null +++ b/src/store/federation/federation/federation.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +export type TState = { + isFederation?: boolean +} + +const initialState: TState = { + isFederation: undefined, +} + +export const federationSlice = createSlice({ + name: 'isFederation', + initialState, + reducers: { + setIsFederation: (state, action: PayloadAction) => { + state.isFederation = action.payload + }, + }, +}) + +export const { setIsFederation } = federationSlice.actions diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..e0b5aa0 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,21 @@ +import { configureStore } from '@reduxjs/toolkit' +import { themeSlice } from './theme/theme/theme' +import { federationSlice } from './federation/federation/federation' +import { baseprefixSlice } from './federation/federation/baseprefix' +import { swaggerSlice } from './swagger/swagger/swagger' +import { clusterListSlice } from './clusterList/clusterList/clusterList' +import { clusterSlice } from './cluster/cluster/cluster' + +export const store = configureStore({ + reducer: { + openapiTheme: themeSlice.reducer, + federation: federationSlice.reducer, + baseprefix: baseprefixSlice.reducer, + swagger: swaggerSlice.reducer, + clusterList: clusterListSlice.reducer, + cluster: clusterSlice.reducer, + }, +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch diff --git a/src/store/swagger/swagger/swagger.ts b/src/store/swagger/swagger/swagger.ts new file mode 100644 index 0000000..12bdf35 --- /dev/null +++ b/src/store/swagger/swagger/swagger.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-param-reassign */ +import { OpenAPIV2 } from 'openapi-types' +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +export type TState = { + swagger: OpenAPIV2.Document | undefined +} + +const initialState: TState = { + swagger: undefined, +} + +export const swaggerSlice = createSlice({ + name: 'swagger', + initialState, + reducers: { + setSwagger: (state, action: PayloadAction) => { + state.swagger = action.payload + }, + }, +}) + +export const { setSwagger } = swaggerSlice.actions diff --git a/src/store/theme/theme/theme.ts b/src/store/theme/theme/theme.ts new file mode 100644 index 0000000..f567799 --- /dev/null +++ b/src/store/theme/theme/theme.ts @@ -0,0 +1,25 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +export type TState = { + theme: 'light' | 'dark' +} + +const initialState: TState = { + theme: 'light', +} + +export const themeSlice = createSlice({ + name: 'theme', + initialState, + reducers: { + setTheme: (state, action: PayloadAction<'light' | 'dark'>) => { + state.theme = action.payload + }, + }, +}) + +export const { setTheme } = themeSlice.actions + +// export default themeSlice diff --git a/src/templates/BaseTemplate/BaseTemplate.tsx b/src/templates/BaseTemplate/BaseTemplate.tsx new file mode 100644 index 0000000..5dc94dc --- /dev/null +++ b/src/templates/BaseTemplate/BaseTemplate.tsx @@ -0,0 +1,105 @@ +import React, { FC, ReactNode, useEffect, useCallback } from 'react' +import { Layout, theme as antdtheme, Alert } from 'antd' +import { useClusterList } from '@prorobotech/openapi-k8s-toolkit' +import { useSelector, useDispatch } from 'react-redux' +import { useParams, useNavigate } from 'react-router-dom' +import type { RootState } from 'store/store' +import { setTheme } from 'store/theme/theme/theme' +import { setCluster } from 'store/cluster/cluster/cluster' +import { setClusterList } from 'store/clusterList/clusterList/clusterList' +import { DefaultLayout, DefaultColorProvider, TitleWithNoTopMargin, ThemeSelector } from 'components' +import { Styled } from './styled' + +type TBaseTemplateProps = { + withNoCluster?: boolean + children?: ReactNode | undefined + forcedTheme?: 'dark' | 'light' +} + +export const BaseTemplate: FC = ({ children, withNoCluster, forcedTheme }) => { + const navigate = useNavigate() + const { clusterName } = useParams() + const { useToken } = antdtheme + const { token } = useToken() + const dispatch = useDispatch() + const theme = useSelector((state: RootState) => state.openapiTheme.theme) + const isFederation = useSelector((state: RootState) => state.federation.isFederation) + const baseprefix = useSelector((state: RootState) => state.baseprefix.baseprefix) + const clusterListQuery = useClusterList({ refetchInterval: false }) + + useEffect(() => { + if (forcedTheme) { + return + } + const localStorageTheme = localStorage.getItem('theme') + if (localStorageTheme && (localStorageTheme === 'dark' || localStorageTheme === 'light')) { + dispatch(setTheme(localStorageTheme)) + } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + localStorage.setItem('theme', 'dark') + dispatch(setTheme('dark')) + } else { + localStorage.setItem('theme', 'light') + dispatch(setTheme('light')) + } + }, [dispatch, forcedTheme]) + + useEffect(() => { + if (forcedTheme) { + dispatch(setTheme(forcedTheme)) + } + }, [dispatch, forcedTheme]) + + const handleStorage = useCallback(() => { + const localStorageTheme = localStorage.getItem('theme') + if (localStorageTheme && (localStorageTheme === 'dark' || localStorageTheme === 'light')) { + dispatch(setTheme(localStorageTheme)) + } + }, [dispatch]) + + useEffect(() => { + window.addEventListener('storage', handleStorage) + + return () => { + window.removeEventListener('storage', handleStorage) + } + }, [handleStorage]) + + useEffect(() => { + if (clusterListQuery.data) { + dispatch(setClusterList(clusterListQuery.data)) + } + }, [clusterListQuery, dispatch]) + + if (clusterName) { + dispatch(setCluster(clusterName)) + } + + if (!clusterName && !withNoCluster) { + navigate(`${baseprefix}/`) + } + + return ( + + + + + + + {!isFederation && ( + + OpenAPI UI + {clusterListQuery.error && ( + + )} + + + )} + {children} + + + + + + + ) +} diff --git a/src/templates/BaseTemplate/index.ts b/src/templates/BaseTemplate/index.ts new file mode 100644 index 0000000..284a62d --- /dev/null +++ b/src/templates/BaseTemplate/index.ts @@ -0,0 +1 @@ +export { BaseTemplate } from './BaseTemplate' diff --git a/src/templates/BaseTemplate/styled.ts b/src/templates/BaseTemplate/styled.ts new file mode 100644 index 0000000..ee6d5e5 --- /dev/null +++ b/src/templates/BaseTemplate/styled.ts @@ -0,0 +1,19 @@ +import styled from 'styled-components' + +type TContainerProps = { + $isDark: boolean +} + +const Container = styled.div` + min-height: 100vh; +` + +const TitleAndThemeToggle = styled.div` + display: flex; + justify-content: space-between; + width: 100%; +` +export const Styled = { + Container, + TitleAndThemeToggle, +} diff --git a/src/templates/index.ts b/src/templates/index.ts new file mode 100644 index 0000000..284a62d --- /dev/null +++ b/src/templates/index.ts @@ -0,0 +1 @@ +export { BaseTemplate } from './BaseTemplate' diff --git a/src/utils/getBaseprefix.ts b/src/utils/getBaseprefix.ts new file mode 100644 index 0000000..9d07e22 --- /dev/null +++ b/src/utils/getBaseprefix.ts @@ -0,0 +1,6 @@ +export const getBasePrefix = (isFederation?: boolean) => { + if (isFederation) { + return '/openapi-ui-federation' + } + return import.meta.env.BASE_URL || '/openapi-ui' +} diff --git a/vite.config.ts b/vite.config.ts index 9a6df8b..788f486 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import path from 'path' import dotenv from 'dotenv' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' +import federation from '@originjs/vite-plugin-federation' import { nodePolyfills } from 'vite-plugin-node-polyfills' const { VITE_BASEPREFIX } = process.env @@ -21,6 +22,14 @@ export default defineConfig({ publicDir: 'public', plugins: [ react(), + federation({ + name: 'openapi-ui', + filename: 'remoteEntry.js', + exposes: { + './App': './src/federation.tsx', + }, + shared: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'antd', '@tanstack/react-query'], + }), nodePolyfills({ include: ['buffer', 'process'], globals: {