mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 10:19:49 +00:00
project structure + stores + routes + base template
This commit is contained in:
141
src/App.tsx
Normal file
141
src/App.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
`
|
||||
1
src/components/atoms/DefaultColorProvider/index.ts
Normal file
1
src/components/atoms/DefaultColorProvider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DefaultColorProvider'
|
||||
31
src/components/atoms/DefaultLayout/DefaultLayout.ts
Normal file
31
src/components/atoms/DefaultLayout/DefaultLayout.ts
Normal 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,
|
||||
}
|
||||
1
src/components/atoms/DefaultLayout/index.ts
Normal file
1
src/components/atoms/DefaultLayout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './DefaultLayout'
|
||||
28
src/components/atoms/ThemeSelector/ThemeSelector.tsx
Normal file
28
src/components/atoms/ThemeSelector/ThemeSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
src/components/atoms/ThemeSelector/index.ts
Normal file
1
src/components/atoms/ThemeSelector/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ThemeSelector'
|
||||
11
src/components/atoms/ThemeSelector/styled.ts
Normal file
11
src/components/atoms/ThemeSelector/styled.ts
Normal 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,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import styled from 'styled-components'
|
||||
import { Typography } from 'antd'
|
||||
|
||||
export const TitleWithNoTopMargin = styled(Typography.Title)`
|
||||
margin-top: 0;
|
||||
`
|
||||
1
src/components/atoms/TitleWithNoTopMargin/index.tsx
Normal file
1
src/components/atoms/TitleWithNoTopMargin/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './TitleWithNoTopMargin'
|
||||
4
src/components/atoms/index.ts
Normal file
4
src/components/atoms/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './DefaultColorProvider'
|
||||
export * from './DefaultLayout'
|
||||
export * from './TitleWithNoTopMargin'
|
||||
export * from './ThemeSelector'
|
||||
1
src/components/index.ts
Normal file
1
src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './atoms'
|
||||
4
src/constants/basePrefix.ts
Normal file
4
src/constants/basePrefix.ts
Normal 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
214
src/constants/colors.ts
Normal 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
17
src/federation.tsx
Normal 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
|
||||
23
src/store/cluster/cluster/cluster.ts
Normal file
23
src/store/cluster/cluster/cluster.ts
Normal 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
|
||||
24
src/store/clusterList/clusterList/clusterList.ts
Normal file
24
src/store/clusterList/clusterList/clusterList.ts
Normal 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
|
||||
23
src/store/federation/federation/baseprefix.ts
Normal file
23
src/store/federation/federation/baseprefix.ts
Normal 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
|
||||
23
src/store/federation/federation/federation.ts
Normal file
23
src/store/federation/federation/federation.ts
Normal 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
21
src/store/store.ts
Normal 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
|
||||
24
src/store/swagger/swagger/swagger.ts
Normal file
24
src/store/swagger/swagger/swagger.ts
Normal 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
|
||||
25
src/store/theme/theme/theme.ts
Normal file
25
src/store/theme/theme/theme.ts
Normal 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
|
||||
105
src/templates/BaseTemplate/BaseTemplate.tsx
Normal file
105
src/templates/BaseTemplate/BaseTemplate.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
src/templates/BaseTemplate/index.ts
Normal file
1
src/templates/BaseTemplate/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { BaseTemplate } from './BaseTemplate'
|
||||
19
src/templates/BaseTemplate/styled.ts
Normal file
19
src/templates/BaseTemplate/styled.ts
Normal 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
1
src/templates/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { BaseTemplate } from './BaseTemplate'
|
||||
6
src/utils/getBaseprefix.ts
Normal file
6
src/utils/getBaseprefix.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getBasePrefix = (isFederation?: boolean) => {
|
||||
if (isFederation) {
|
||||
return '/openapi-ui-federation'
|
||||
}
|
||||
return import.meta.env.BASE_URL || '/openapi-ui'
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user