project structure + stores + routes + base template

This commit is contained in:
typescreep
2025-05-23 08:51:41 +03:00
parent 11b8de2537
commit b85ce98e73
28 changed files with 778 additions and 0 deletions

141
src/App.tsx Normal file
View File

@@ -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<TAppProps> = ({ 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 = '') => (
<Routes>
<Route path={`${prefix}/`} element={<MainPage forcedTheme={forcedTheme} />} />
<Route path={`${prefix}/cluster-list`} element={<ClusterAndNsListPage forcedTheme={forcedTheme} />} />
<Route path={`${prefix}/:clusterName/:namespace?/apis`} element={<ListApiPage forcedTheme={forcedTheme} />} />
<Route
path={`${prefix}/:clusterName/:namespace?/crds-by-api/:apiGroup/:apiVersion/:apiExtensionVersion`}
element={<ListCrdByApiGroupPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/non-crds-by-api/:apiGroup/:apiVersion/`}
element={<ListApiByApiGroupPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/crd-table/:apiGroup/:apiVersion/:apiExtensionVersion/:crdName`}
element={<TableCrdPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/non-crd-table/:apiGroup/:apiVersion/:typeName`}
element={<TableApiPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/builtin-table/:typeName`}
element={<TableBuiltinPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/forms/builtin/:apiVersion/:typeName/:entryName?/`}
element={<FormBuiltinPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/forms/apis/:apiGroup/:apiVersion/:typeName/:entryName?/`}
element={<FormApiPage forcedTheme={forcedTheme} />}
/>
<Route
path={`${prefix}/:clusterName/:namespace?/:syntheticProject?/forms/crds/:apiGroup/:apiVersion/:typeName/:entryName?/`}
element={<FormCrdPage forcedTheme={forcedTheme} />}
/>
<Route path={`${prefix}/factory/*`} element={<FactoryPage forcedTheme={forcedTheme} />} />
</Routes>
)
const colors = theme === 'dark' ? colorsDark : colorsLight
return (
<QueryClientProvider client={queryClient}>
{import.meta.env.MODE === 'development' && <ReactQueryDevtools />}
<ConfigProvider
theme={{
algorithm: theme === 'dark' ? antdtheme.darkAlgorithm : undefined,
token: {
fontFamily: '"Roboto", sans-serif',
...colors,
...sizes,
},
components: {
Layout: {
...colors,
},
},
}}
>
{isFederation ? renderRoutes() : <BrowserRouter>{renderRoutes(basePrefix)}</BrowserRouter>}
</ConfigProvider>
</QueryClientProvider>
)
}

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components'
type TDefaultColorProviderProps = {
$color: string
}
export const DefaultColorProvider = styled.div<TDefaultColorProviderProps>`
color: ${({ $color }) => $color};
td {
color: ${({ $color }) => $color};
}
`

View File

@@ -0,0 +1 @@
export * from './DefaultColorProvider'

View File

@@ -0,0 +1,31 @@
import styled from 'styled-components'
type TLayoutProps = {
$bgColor: string
}
const Layout = styled.div<TLayoutProps>`
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<TContentPaddingProps>`
padding: ${({ $isFederation }) => ($isFederation ? 0 : '24px')};
min-height: 100vh;
`
export const DefaultLayout = {
Layout,
ContentContainer,
ContentPadding,
}

View File

@@ -0,0 +1 @@
export * from './DefaultLayout'

View File

@@ -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 (
<Styled.Container>
Dark Mode
<Switch size="small" value={theme === 'dark'} onChange={checked => updateTheme(checked)} />
</Styled.Container>
)
}

View File

@@ -0,0 +1 @@
export * from './ThemeSelector'

View File

@@ -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,
}

View File

@@ -0,0 +1,6 @@
import styled from 'styled-components'
import { Typography } from 'antd'
export const TitleWithNoTopMargin = styled(Typography.Title)`
margin-top: 0;
`

View File

@@ -0,0 +1 @@
export * from './TitleWithNoTopMargin'

View File

@@ -0,0 +1,4 @@
export * from './DefaultColorProvider'
export * from './DefaultLayout'
export * from './TitleWithNoTopMargin'
export * from './ThemeSelector'

1
src/components/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './atoms'

View File

@@ -0,0 +1,4 @@
import { getBasePrefix } from 'utils/getBaseprefix'
// eslint-disable-next-line no-underscore-dangle
export const BASEPREFIX: string = getBasePrefix()

214
src/constants/colors.ts Normal file
View File

@@ -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,
}

17
src/federation.tsx Normal file
View File

@@ -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<TFederationAppProps> = ({ forcedTheme }) => (
<Provider store={store}>
<App isFederation forcedTheme={forcedTheme} />
</Provider>
)
// eslint-disable-next-line import/no-default-export
export default FederationApp

View File

@@ -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<string>) => {
state.cluster = action.payload
},
},
})
export const { setCluster } = clusterSlice.actions

View File

@@ -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<TClusterList | undefined>) => {
state.clusterList = action.payload
},
},
})
export const { setClusterList } = clusterListSlice.actions

View File

@@ -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<string>) => {
state.baseprefix = action.payload
},
},
})
export const { setBaseprefix } = baseprefixSlice.actions

View File

@@ -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<boolean>) => {
state.isFederation = action.payload
},
},
})
export const { setIsFederation } = federationSlice.actions

21
src/store/store.ts Normal file
View File

@@ -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<typeof store.getState>
export type AppDispatch = typeof store.dispatch

View File

@@ -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<OpenAPIV2.Document>) => {
state.swagger = action.payload
},
},
})
export const { setSwagger } = swaggerSlice.actions

View File

@@ -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

View File

@@ -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<TBaseTemplateProps> = ({ 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 (
<DefaultColorProvider $color={token.colorText}>
<Styled.Container $isDark={theme === 'dark'}>
<Layout>
<DefaultLayout.Layout $bgColor={token.colorBgLayout}>
<DefaultLayout.ContentContainer>
<DefaultLayout.ContentPadding $isFederation={isFederation}>
{!isFederation && (
<Styled.TitleAndThemeToggle>
<TitleWithNoTopMargin level={1}>OpenAPI UI</TitleWithNoTopMargin>
{clusterListQuery.error && (
<Alert message={`Cluster List Error: ${clusterListQuery.error?.message} `} type="error" />
)}
<ThemeSelector />
</Styled.TitleAndThemeToggle>
)}
{children}
</DefaultLayout.ContentPadding>
</DefaultLayout.ContentContainer>
</DefaultLayout.Layout>
</Layout>
</Styled.Container>
</DefaultColorProvider>
)
}

View File

@@ -0,0 +1 @@
export { BaseTemplate } from './BaseTemplate'

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components'
type TContainerProps = {
$isDark: boolean
}
const Container = styled.div<TContainerProps>`
min-height: 100vh;
`
const TitleAndThemeToggle = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`
export const Styled = {
Container,
TitleAndThemeToggle,
}

1
src/templates/index.ts Normal file
View File

@@ -0,0 +1 @@
export { BaseTemplate } from './BaseTemplate'

View File

@@ -0,0 +1,6 @@
export const getBasePrefix = (isFederation?: boolean) => {
if (isFederation) {
return '/openapi-ui-federation'
}
return import.meta.env.BASE_URL || '/openapi-ui'
}

View File

@@ -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: {