From 9831c0419d7caf2ac675f319ac0af7071e91c7eb Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Wed, 31 Jul 2024 12:30:23 +0200 Subject: [PATCH] frontend: Refactor KubeObject class, deprecate makeKubeObject Signed-off-by: Oleksandr Dubenko --- frontend/src/components/App/Home/index.tsx | 226 +------ .../DetailsViewSection.stories.tsx | 5 +- .../detailsViewSectionSlice.test.ts | 5 +- frontend/src/components/cluster/Charts.tsx | 14 +- .../common/ErrorBoundary/ErrorBoundary.tsx | 11 +- .../src/components/common/LabelListItem.tsx | 2 +- frontend/src/components/common/Link.tsx | 8 +- .../common/NameValueTable/NameValueTable.tsx | 6 +- .../common/Resource/AuthVisible.tsx | 4 +- .../common/Resource/CircularChart.tsx | 12 +- .../common/Resource/CreateButton.tsx | 2 +- .../MainInfoSection.stories.tsx | 2 +- .../MainInfoSection/MainInfoSection.tsx | 15 +- .../MainInfoSection/MainInfoSectionHeader.tsx | 10 +- .../Resource/MetadataDisplay.stories.tsx | 2 +- .../common/Resource/MetadataDisplay.tsx | 16 +- .../common/Resource/PortForward.tsx | 10 +- .../components/common/Resource/Resource.tsx | 42 +- .../common/Resource/ResourceListView.tsx | 30 +- .../common/Resource/ResourceTable.stories.tsx | 6 +- .../common/Resource/ResourceTable.tsx | 50 +- .../common/Resource/RestartButton.tsx | 3 +- .../common/Resource/ScaleButton.tsx | 6 +- .../common/Resource/ViewButton.stories.tsx | 5 +- .../src/components/common/SimpleTable.tsx | 15 +- .../src/components/configmap/List.stories.tsx | 5 +- .../crd/CustomResourceDetails.stories.tsx | 1 + .../components/crd/CustomResourceDetails.tsx | 7 +- .../src/components/crd/CustomResourceList.tsx | 22 +- frontend/src/components/crd/storyHelper.ts | 13 +- frontend/src/components/cronjob/Details.tsx | 2 +- frontend/src/components/endpoints/Details.tsx | 8 +- .../endpoints/EndpointDetails.stories.tsx | 8 +- .../endpoints/EndpointList.stories.tsx | 11 +- .../HPAList.stories.tsx | 4 +- .../components/ingress/ClassList.stories.tsx | 3 +- frontend/src/components/ingress/Details.tsx | 2 +- .../src/components/ingress/List.stories.tsx | 3 +- frontend/src/components/job/storyHelper.ts | 5 +- .../src/components/lease/List.stories.tsx | 3 +- .../components/limitRange/List.stories.tsx | 3 +- frontend/src/components/namespace/List.tsx | 10 +- .../namespace/NamespaceList.stories.tsx | 7 +- frontend/src/components/node/Details.tsx | 6 +- frontend/src/components/node/List.tsx | 2 +- frontend/src/components/pod/List.tsx | 10 +- .../src/components/pod/PodLogs.stories.tsx | 6 +- frontend/src/components/pod/storyHelper.ts | 2 +- .../podDisruptionBudget/pdbList.stories.tsx | 4 +- .../priorityClassDetails.stories.tsx | 2 +- .../priorityClassList.stories.tsx | 4 +- .../resourceQuotaList.stories.tsx | 4 +- .../components/runtimeClass/List.stories.tsx | 3 +- .../src/components/secret/List.stories.tsx | 3 +- frontend/src/components/service/Details.tsx | 4 +- .../src/components/serviceaccount/Details.tsx | 2 +- .../src/components/storage/ClaimDetails.tsx | 6 +- frontend/src/components/storage/ClaimList.tsx | 18 +- .../components/storage/ClassList.stories.tsx | 3 +- frontend/src/components/storage/ClassList.tsx | 2 +- .../components/storage/VolumeList.stories.tsx | 3 +- .../ClassList.Items.stories.storyshot | 4 +- .../VolumeList.Items.stories.storyshot | 4 +- .../verticalPodAutoscaler/VPAList.stories.tsx | 4 +- .../MutatingWebhookConfigDetails.stories.tsx | 4 +- ...ValidatingWebhookConfigDetails.stories.tsx | 6 +- frontend/src/components/workload/Details.tsx | 8 +- frontend/src/components/workload/Overview.tsx | 16 +- frontend/src/lib/k8s/KubeObject.ts | 574 ++++++++++++++++ frontend/src/lib/k8s/cluster.ts | 628 +----------------- frontend/src/lib/k8s/configMap.ts | 7 +- frontend/src/lib/k8s/crd.ts | 22 +- frontend/src/lib/k8s/cronJob.ts | 5 +- frontend/src/lib/k8s/daemonSet.ts | 9 +- frontend/src/lib/k8s/deployment.ts | 5 +- frontend/src/lib/k8s/endpoints.ts | 11 +- frontend/src/lib/k8s/event.ts | 13 +- frontend/src/lib/k8s/hpa.ts | 15 +- frontend/src/lib/k8s/index.test.ts | 2 +- frontend/src/lib/k8s/index.ts | 83 +-- frontend/src/lib/k8s/ingress.ts | 7 +- frontend/src/lib/k8s/ingressClass.ts | 9 +- frontend/src/lib/k8s/job.ts | 9 +- frontend/src/lib/k8s/lease.ts | 7 +- frontend/src/lib/k8s/limitRange.tsx | 7 +- .../lib/k8s/mutatingWebhookConfiguration.ts | 9 +- frontend/src/lib/k8s/namespace.ts | 8 +- frontend/src/lib/k8s/networkpolicy.tsx | 5 +- frontend/src/lib/k8s/node.ts | 9 +- frontend/src/lib/k8s/persistentVolume.ts | 9 +- frontend/src/lib/k8s/persistentVolumeClaim.ts | 11 +- frontend/src/lib/k8s/pod.ts | 13 +- frontend/src/lib/k8s/podDisruptionBudget.ts | 9 +- frontend/src/lib/k8s/priorityClass.ts | 17 +- frontend/src/lib/k8s/replicaSet.ts | 9 +- frontend/src/lib/k8s/resourceQuota.ts | 9 +- frontend/src/lib/k8s/role.ts | 9 +- frontend/src/lib/k8s/roleBinding.ts | 9 +- frontend/src/lib/k8s/runtime.ts | 9 +- frontend/src/lib/k8s/secret.ts | 9 +- frontend/src/lib/k8s/service.ts | 9 +- frontend/src/lib/k8s/serviceAccount.ts | 7 +- frontend/src/lib/k8s/statefulSet.ts | 9 +- frontend/src/lib/k8s/storageClass.ts | 16 +- .../lib/k8s/validatingWebhookConfiguration.ts | 9 +- frontend/src/lib/k8s/vpa.ts | 13 +- frontend/src/lib/util.ts | 5 +- frontend/src/plugin/registry.tsx | 6 +- frontend/src/test/index.tsx | 7 +- frontend/src/test/mocker.ts | 24 +- 110 files changed, 1149 insertions(+), 1267 deletions(-) create mode 100644 frontend/src/lib/k8s/KubeObject.ts diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index efb0407cfa0..748da6cf4c5 100644 --- a/frontend/src/components/App/Home/index.tsx +++ b/frontend/src/components/App/Home/index.tsx @@ -1,163 +1,14 @@ -import { Icon } from '@iconify/react'; -import { useTheme } from '@mui/material'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import ListItemText from '@mui/material/ListItemText'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import Typography from '@mui/material/Typography'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import helpers from '../../../helpers'; -import { useClustersConf, useClustersVersion } from '../../../lib/k8s'; -import { ApiError, deleteCluster } from '../../../lib/k8s/apiProxy'; +import { useClustersConf } from '../../../lib/k8s'; import { Cluster } from '../../../lib/k8s/cluster'; -import Event from '../../../lib/k8s/event'; import { createRouteURL } from '../../../lib/router'; -import { useId } from '../../../lib/util'; -import { setConfig } from '../../../redux/configSlice'; -import { Link, PageGrid, SectionBox, SectionFilterHeader } from '../../common'; -import { ConfirmDialog } from '../../common'; +import { PageGrid, SectionBox, SectionFilterHeader } from '../../common'; import ResourceTable from '../../common/Resource/ResourceTable'; import RecentClusters from './RecentClusters'; -function ContextMenu({ cluster }: { cluster: Cluster }) { - const { t } = useTranslation(['translation']); - const history = useHistory(); - const dispatch = useDispatch(); - const [anchorEl, setAnchorEl] = React.useState(null); - const menuId = useId('context-menu'); - const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false); - - function removeCluster(cluster: Cluster) { - deleteCluster(cluster.name || '') - .then(config => { - dispatch(setConfig(config)); - }) - .catch((err: Error) => { - if (err.message === 'Not Found') { - // TODO: create notification with error message - } - }) - .finally(() => { - history.push('/'); - }); - } - - function handleMenuClose() { - setAnchorEl(null); - } - - return ( - <> - { - setAnchorEl(event.currentTarget); - }} - aria-haspopup="menu" - aria-controls={menuId} - aria-label={t('Actions')} - > - - - { - handleMenuClose(); - }} - > - { - history.push(createRouteURL('cluster', { cluster: cluster.name })); - handleMenuClose(); - }} - > - {t('translation|View')} - - { - history.push(createRouteURL('settingsCluster', { cluster: cluster.name })); - handleMenuClose(); - }} - > - {t('translation|Settings')} - - {helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && ( - { - setOpenConfirmDialog(true); - handleMenuClose(); - }} - > - {t('translation|Delete')} - - )} - - - setOpenConfirmDialog(false)} - onConfirm={() => { - setOpenConfirmDialog(false); - removeCluster(cluster); - }} - title={t('translation|Delete Cluster')} - description={t( - 'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?', - { - clusterName: cluster.name, - } - )} - /> - - ); -} - -function ClusterStatus({ error }: { error?: ApiError | null }) { - const { t } = useTranslation(['translation']); - const theme = useTheme(); - - const stateUnknown = error === undefined; - const hasReachError = error && error.status !== 401 && error.status !== 403; - - return ( - - - {hasReachError ? ( - - ) : stateUnknown ? ( - - ) : ( - - )} - - {hasReachError ? error.message : stateUnknown ? '⋯' : t('translation|Active')} - - - - ); -} - export default function Home() { const history = useHistory(); const clusters = useClustersConf() || {}; @@ -181,23 +32,23 @@ function HomeComponent(props: HomeComponentProps) { name: c.meta_data?.extensions?.headlamp_info?.customName || c.name, })); const { t } = useTranslation(['translation', 'glossary']); - const [versions, errors] = useClustersVersion(Object.values(clusters)); - const maxWarnings = 50; - const warningsMap = Event.useWarningList(Object.values(customNameClusters).map(c => c.name)); - - function renderWarningsText(clusterName: string) { - const numWarnings = - (!!warningsMap[clusterName]?.error && -1) || - (warningsMap[clusterName]?.warnings?.length ?? -1); - - if (numWarnings === -1) { - return '⋯'; - } - if (numWarnings >= maxWarnings) { - return `${maxWarnings}+`; - } - return numWarnings; - } + // const [versions] = useClustersVersion(Object.values(clusters)); + // const maxWarnings = 50; + // const warningsMap = Event.useWarningList(Object.values(customNameClusters).map(c => c.name)); + + // function renderWarningsText(clusterName: string) { + // const numWarnings = + // (!!warningsMap[clusterName]?.error && -1) || + // (warningsMap[clusterName]?.warnings?.length ?? -1); + + // if (numWarnings === -1) { + // return '...'; + // } + // if (numWarnings >= maxWarnings) { + // return `${maxWarnings}+`; + // } + // return numWarnings; + // } return ( @@ -213,44 +64,7 @@ function HomeComponent(props: HomeComponentProps) { /> } > - cluster.name, - render: ({ name }) => ( - - {name} - - ), - }, - { - label: t('Status'), - getValue: cluster => cluster.name, - render: ({ name }) => , - }, - { - label: t('Warnings'), - getValue: ({ name }) => renderWarningsText(name), - }, - { - label: t('glossary|Kubernetes Version'), - getValue: ({ name }) => versions[name]?.gitVersion || '⋯', - }, - { - label: '', - getValue: () => '', - cellProps: { - align: 'right', - }, - render: cluster => , - }, - ]} - data={Object.values(customNameClusters)} - id="headlamp-home-clusters" - /> + ); diff --git a/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx b/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx index 0ec1a2246ae..3eae02d0fa3 100644 --- a/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx +++ b/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { useDispatch } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; +import { KubeObject } from '../../lib/k8s/cluster'; import { SectionBox } from '../common'; import DetailsViewSection, { DetailsViewSectionProps } from './DetailsViewSection'; import { setDetailsView } from './detailsViewSectionSlice'; @@ -63,10 +64,10 @@ const Template: Story = args => { export const MatchRenderIt = Template.bind({}); MatchRenderIt.args = { - resource: { kind: 'Node' }, + resource: { kind: 'Node' } as KubeObject, }; export const NoMatchNoRender = Template.bind({}); NoMatchNoRender.args = { - resource: { kind: 'DoesNotExist' }, + resource: { kind: 'DoesNotExist' } as KubeObject, }; diff --git a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts index da30ac12443..497a23adbf2 100644 --- a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts +++ b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts @@ -3,7 +3,6 @@ import detailsViewSectionReducer, { addDetailsViewSectionsProcessor, DefaultDetailsViewSection, DetailsViewSection, - DetailsViewSectionProcessorType, DetailsViewsSectionProcessor, setDetailsView, setDetailsViewSection, @@ -67,7 +66,7 @@ describe('detailsViewSectionSlice', () => { it('should add a new details view sections processor when provided as an object', () => { const processor: DetailsViewsSectionProcessor = { id: 'test-processor', - processor: info => info.actions, + processor: () => [{ id: 'test-section' }], }; store.dispatch(addDetailsViewSectionsProcessor(processor)); expect(store.getState().detailsViewSection.detailsViewSectionsProcessors).toEqual([ @@ -76,7 +75,7 @@ describe('detailsViewSectionSlice', () => { }); it('should add a new details view sections processor when provided as a function', () => { - const processorFunc: DetailsViewSectionProcessorType = info => info.actions; + const processorFunc = () => [{ id: 'test-section' }]; store.dispatch(addDetailsViewSectionsProcessor(processorFunc)); const processors = store.getState().detailsViewSection.detailsViewSectionsProcessors; expect(processors).toHaveLength(1); diff --git a/frontend/src/components/cluster/Charts.tsx b/frontend/src/components/cluster/Charts.tsx index 104518ff803..e455ff41f2c 100644 --- a/frontend/src/components/cluster/Charts.tsx +++ b/frontend/src/components/cluster/Charts.tsx @@ -1,7 +1,7 @@ import '../../i18n/config'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; -import { KubeObject } from '../../lib/k8s/cluster'; +import { KubeMetrics } from '../../lib/k8s/cluster'; import Node from '../../lib/k8s/node'; import Pod from '../../lib/k8s/pod'; import { parseCpu, parseRam, TO_GB, TO_ONE_CPU } from '../../lib/units'; @@ -14,11 +14,11 @@ export function MemoryCircularChart(props: ResourceCircularChartProps) { const { noMetrics } = props; const { t } = useTranslation(['translation', 'glossary']); - function memoryUsedGetter(item: KubeObject) { + function memoryUsedGetter(item: KubeMetrics) { return parseRam(item.usage.memory) / TO_GB; } - function memoryAvailableGetter(item: KubeObject) { + function memoryAvailableGetter(item: Node | Pod) { return parseRam(item.status!.capacity.memory) / TO_GB; } @@ -50,11 +50,11 @@ export function CpuCircularChart(props: ResourceCircularChartProps) { const { noMetrics } = props; const { t } = useTranslation(['translation', 'glossary']); - function cpuUsedGetter(item: KubeObject) { + function cpuUsedGetter(item: KubeMetrics) { return parseCpu(item.usage.cpu) / TO_ONE_CPU; } - function cpuAvailableGetter(item: KubeObject) { + function cpuAvailableGetter(item: Node | Pod) { return parseCpu(item.status!.capacity.cpu) / TO_ONE_CPU; } @@ -82,7 +82,7 @@ export function CpuCircularChart(props: ResourceCircularChartProps) { ); } -export function PodsStatusCircleChart(props: Pick) { +export function PodsStatusCircleChart(props: { items: Pod[] | null }) { const theme = useTheme(); const { items } = props; const { t } = useTranslation(['translation', 'glossary']); @@ -143,7 +143,7 @@ export function PodsStatusCircleChart(props: Pick) { +export function NodesStatusCircleChart(props: { items: Node[] | null }) { const theme = useTheme(); const { items } = props; const { t } = useTranslation(['translation', 'glossary']); diff --git a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx index 287a0b846d0..2e3b69afcad 100644 --- a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -3,7 +3,7 @@ import { eventAction, HeadlampEventType } from '../../../redux/headlampEventSlic import store from '../../../redux/stores/store'; export interface ErrorBoundaryProps { - fallback?: ComponentType<{ error: Error }> | ReactElement | null; + fallback?: ComponentType<{ error: Error }> | ReactElement<{ error: Error }> | null; } interface State { @@ -49,13 +49,10 @@ export default class ErrorBoundary extends Component if (!error) { return this.props.children; } - if (isValidElement(this.props.fallback)) { - return this.props.fallback; + const FallbackComponent = this.props.fallback; + if (isValidElement(FallbackComponent)) { + return FallbackComponent; } - const FallbackComponent = this.props.fallback as - | ComponentType<{ error: Error }> - | undefined - | null; return FallbackComponent ? : null; } } diff --git a/frontend/src/components/common/LabelListItem.tsx b/frontend/src/components/common/LabelListItem.tsx index f191310805f..a7946c78bda 100644 --- a/frontend/src/components/common/LabelListItem.tsx +++ b/frontend/src/components/common/LabelListItem.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { LightTooltip } from './Tooltip'; export interface LabelListItemProps { - labels: React.ReactNode[]; + labels?: React.ReactNode[]; } export default function LabelListItem(props: LabelListItemProps) { diff --git a/frontend/src/components/common/Link.tsx b/frontend/src/components/common/Link.tsx index ecbe7248e62..dc84e76ca71 100644 --- a/frontend/src/components/common/Link.tsx +++ b/frontend/src/components/common/Link.tsx @@ -1,7 +1,7 @@ import MuiLink from '@mui/material/Link'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { makeKubeObject } from '../../lib/k8s/cluster'; +import { KubeObject } from '../../lib/k8s/cluster'; import { createRouteURL, RouteURLProps } from '../../lib/router'; import { LightTooltip } from './Tooltip'; @@ -24,7 +24,7 @@ export interface LinkProps extends LinkBaseProps { } export interface LinkObjectProps extends LinkBaseProps { - kubeObject: InstanceType>; + kubeObject?: KubeObject | null; [prop: string]: any; } @@ -32,8 +32,8 @@ function PureLink(props: React.PropsWithChildren) { if ((props as LinkObjectProps).kubeObject) { const { kubeObject, ...otherProps } = props as LinkObjectProps; return ( - - {props.children || kubeObject.getName()} + + {props.children || kubeObject!.getName()} ); } diff --git a/frontend/src/components/common/NameValueTable/NameValueTable.tsx b/frontend/src/components/common/NameValueTable/NameValueTable.tsx index 946a0fa5186..7160a5fcdbf 100644 --- a/frontend/src/components/common/NameValueTable/NameValueTable.tsx +++ b/frontend/src/components/common/NameValueTable/NameValueTable.tsx @@ -6,7 +6,7 @@ export interface NameValueTableRow { /** The name (key) for this row */ name: string | JSX.Element; /** The value for this row */ - value?: string | JSX.Element | JSX.Element[]; + value?: string | number | null | boolean | JSX.Element | JSX.Element[]; /** Whether this row should be hidden (can be a boolean or a function that will take the * @param value and return a boolean) */ hide?: boolean | ((value: NameValueTableRow['value']) => boolean); @@ -24,7 +24,7 @@ export interface NameValueTableProps { function Value({ value, }: { - value: string | JSX.Element | JSX.Element[] | undefined; + value: string | null | number | boolean | JSX.Element | JSX.Element[] | undefined; }): JSX.Element | null { if (typeof value === 'undefined') { return null; @@ -39,7 +39,7 @@ function Value({ ); } else { - return value; + return value as JSX.Element | null; } } diff --git a/frontend/src/components/common/Resource/AuthVisible.tsx b/frontend/src/components/common/Resource/AuthVisible.tsx index 1f926946829..d805f753c22 100644 --- a/frontend/src/components/common/Resource/AuthVisible.tsx +++ b/frontend/src/components/common/Resource/AuthVisible.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject, KubeObjectClass } from '../../../lib/k8s/cluster'; export interface AuthVisibleProps extends React.PropsWithChildren<{}> { /** The item for which auth will be checked or a resource class (e.g. Job). */ - item: KubeObject; + item: KubeObject | KubeObjectClass | null; /** The verb associated with the permissions being verifying. See https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb . */ authVerb: string; /** The subresource for which the permissions are being verifyied (e.g. "log" when checking for a pod's log). */ diff --git a/frontend/src/components/common/Resource/CircularChart.tsx b/frontend/src/components/common/Resource/CircularChart.tsx index eb2022a0619..91dc20fe30e 100644 --- a/frontend/src/components/common/Resource/CircularChart.tsx +++ b/frontend/src/components/common/Resource/CircularChart.tsx @@ -1,21 +1,23 @@ import '../../../i18n/config'; -import _ from 'lodash'; +import _, { List } from 'lodash'; import { useTranslation } from 'react-i18next'; import { KubeMetrics, KubeObject } from '../../../lib/k8s/cluster'; +import Node from '../../../lib/k8s/node'; +import Pod from '../../../lib/k8s/pod'; import { PercentageCircleProps } from '../Chart'; import TileChart from '../TileChart'; export interface CircularChartProps extends Omit { /** Items to display in the chart (should have a corresponding value in @param itemsMetrics) */ - items: KubeObject[] | null; + items: Node[] | Pod[] | null; /** Metrics to display in the chart (for items in @param items) */ itemsMetrics: KubeMetrics[] | null; /** Whether no metrics are available. If true, then instead of a chart, a message will be displayed */ noMetrics?: boolean; /** Function to get the "used" value for the metrics in question */ - resourceUsedGetter?: (node: KubeObject) => number; + resourceUsedGetter?: (node: KubeMetrics) => number; /** Function to get the "available" value for the metrics in question */ - resourceAvailableGetter?: (node: KubeObject) => number; + resourceAvailableGetter?: (node: Node | Pod) => number; /** Function to create a legend for the data */ getLegend?: (used: number, available: number) => string; /** Tooltip to display when hovering over the chart */ @@ -56,7 +58,7 @@ export function CircularChart(props: CircularChartProps) { const nodeMetrics = filterMetrics(items, itemsMetrics); const usedValue = _.sumBy(nodeMetrics, resourceUsedGetter); - const availableValue = _.sumBy(items, resourceAvailableGetter); + const availableValue = _.sumBy(items as List, resourceAvailableGetter); return [usedValue, availableValue]; } diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index 7dce4352bd7..b268fdcb246 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -65,7 +65,7 @@ export default function CreateButton(props: CreateButtonProps) { if (massagedNewItemDefs[i].kind === 'List') { // flatten this List kind with the items that it has which is a list of valid k8s resources const deletedItem = massagedNewItemDefs.splice(i, 1); - massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items); + massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items!); } if (!massagedNewItemDefs[i].metadata?.name) { setErrorMessage( diff --git a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.stories.tsx b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.stories.tsx index 6dce33cb12b..5eb1204ae93 100644 --- a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.stories.tsx +++ b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.stories.tsx @@ -12,7 +12,7 @@ export default { argTypes: {}, } as Meta; -const Template: Story = (args: MainInfoSectionProps) => ( +const Template: Story> = args => ( diff --git a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx index 3817f44a0fd..99e3e434bbe 100644 --- a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx +++ b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx @@ -13,16 +13,13 @@ import SectionBox from '../../SectionBox'; import { MetadataDisplay } from '../MetadataDisplay'; import { MainInfoHeader } from './MainInfoSectionHeader'; -export interface MainInfoSectionProps { - resource: KubeObject | null; - headerSection?: ((resource: KubeObject | null) => React.ReactNode) | React.ReactNode; +export interface MainInfoSectionProps { + resource: T | null; + headerSection?: ((resource: T | null) => React.ReactNode) | React.ReactNode; title?: string; - extraInfo?: - | ((resource: KubeObject | null) => NameValueTableRow[] | null) - | NameValueTableRow[] - | null; + extraInfo?: ((resource: T | null) => NameValueTableRow[] | null) | NameValueTableRow[] | null; actions?: - | ((resource: KubeObject | null) => React.ReactNode[] | null) + | ((resource: T | null) => React.ReactNode[] | null) | React.ReactNode[] | null | HeaderAction[]; @@ -33,7 +30,7 @@ export interface MainInfoSectionProps { error?: string | Error | null; } -export function MainInfoSection(props: MainInfoSectionProps) { +export function MainInfoSection(props: MainInfoSectionProps) { const { resource, headerSection, diff --git a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx index 281a5f4a32a..4b28ca949c3 100644 --- a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx +++ b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx @@ -15,12 +15,12 @@ import EditButton from '../EditButton'; import { RestartButton } from '../RestartButton'; import ScaleButton from '../ScaleButton'; -export interface MainInfoHeaderProps { - resource: KubeObject | null; - headerSection?: ((resource: KubeObject | null) => React.ReactNode) | React.ReactNode; +export interface MainInfoHeaderProps { + resource: T | null; + headerSection?: ((resource: T | null) => React.ReactNode) | React.ReactNode; title?: string; actions?: - | ((resource: KubeObject | null) => React.ReactNode[] | null) + | ((resource: T | null) => React.ReactNode[] | null) | React.ReactNode[] | null | HeaderAction[]; @@ -30,7 +30,7 @@ export interface MainInfoHeaderProps { backLink?: string | ReturnType | null; } -export function MainInfoHeader(props: MainInfoHeaderProps) { +export function MainInfoHeader(props: MainInfoHeaderProps) { const { resource, title, actions = [], headerStyle = 'main', noDefaultActions = false } = props; const headerActions = useTypedSelector(state => state.actionButtons.headerActions); const headerActionsProcessors = useTypedSelector( diff --git a/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx b/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx index 6a4e640168a..f16d150ab96 100644 --- a/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx +++ b/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx @@ -18,7 +18,7 @@ export default { ], } as Meta; -const Template: Story = args => ; +const Template: Story> = args => ; const mockResource: KubeObjectInterface = { kind: 'MyKind', diff --git a/frontend/src/components/common/Resource/MetadataDisplay.tsx b/frontend/src/components/common/Resource/MetadataDisplay.tsx index e7b11f6e7c0..41459031269 100644 --- a/frontend/src/components/common/Resource/MetadataDisplay.tsx +++ b/frontend/src/components/common/Resource/MetadataDisplay.tsx @@ -6,14 +6,14 @@ import Typography, { TypographyProps } from '@mui/material/Typography'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ResourceClasses } from '../../../lib/k8s'; -import { KubeObject, KubeObjectInterface, KubeOwnerReference } from '../../../lib/k8s/cluster'; +import { KubeObject, KubeOwnerReference } from '../../../lib/k8s/cluster'; import Theme from '../../../lib/themes'; import { localeDate } from '../../../lib/util'; import { NameValueTable, NameValueTableRow } from '../../common/SimpleTable'; import Link from '../Link'; import { LightTooltip } from '../Tooltip'; -type ExtraRowsFunc = (resource: KubeObjectInterface) => NameValueTableRow[] | null; +type ExtraRowsFunc = (resource: T) => NameValueTableRow[] | null; export const metadataStyles = (theme: typeof Theme.light) => ({ color: theme.palette.text.primary, @@ -29,15 +29,15 @@ export const metadataStyles = (theme: typeof Theme.light) => ({ textOverflow: 'ellipsis', }); -export interface MetadataDisplayProps { - resource: KubeObject; - extraRows?: ExtraRowsFunc | NameValueTableRow[] | null; +export interface MetadataDisplayProps { + resource: T; + extraRows?: ExtraRowsFunc | NameValueTableRow[] | null; } -export function MetadataDisplay(props: MetadataDisplayProps) { +export function MetadataDisplay(props: MetadataDisplayProps) { const { resource, extraRows } = props; const { t } = useTranslation(); - let makeExtraRows: ExtraRowsFunc; + let makeExtraRows: ExtraRowsFunc; function makeOwnerReferences(ownerReferences: KubeOwnerReference[]) { if (!resource || ownerReferences === undefined) { @@ -54,7 +54,7 @@ export function MetadataDisplay(props: MetadataDisplayProps) { if (ownerRef.kind in ResourceClasses) { let routeName; try { - routeName = new ResourceClasses[ownerRef.kind]().detailsRoute; + routeName = ResourceClasses[ownerRef.kind as keyof typeof ResourceClasses].detailsRoute; } catch (e) { console.error(`Error getting routeName for {ownerRef.kind}`, e); return null; diff --git a/frontend/src/components/common/Resource/PortForward.tsx b/frontend/src/components/common/Resource/PortForward.tsx index 9e1213ea879..12c50fffff4 100644 --- a/frontend/src/components/common/Resource/PortForward.tsx +++ b/frontend/src/components/common/Resource/PortForward.tsx @@ -11,7 +11,7 @@ import { startPortForward, stopOrDeletePortForward, } from '../../../lib/k8s/apiProxy'; -import { KubeContainer, KubeObject } from '../../../lib/k8s/cluster'; +import { KubeContainer, KubeObjectInterface } from '../../../lib/k8s/cluster'; import Pod from '../../../lib/k8s/pod'; import Service from '../../../lib/k8s/service'; import { getCluster } from '../../../lib/util'; @@ -19,7 +19,7 @@ import ActionButton from '../ActionButton'; interface PortForwardProps { containerPort: number | string; - resource?: KubeObject; + resource?: KubeObjectInterface; } export interface PortForwardState { @@ -155,14 +155,14 @@ export default function PortForward(props: PortForwardProps) { } function handlePortForward() { - if (!namespace || !cluster) { + if (!namespace || !cluster || !pods) { return; } setError(null); const resourceName = name || ''; - const podNamespace = isPod ? namespace : pods[0].metadata.namespace; + const podNamespace = isPod ? namespace : pods[0].metadata.namespace!; const serviceNamespace = namespace; const serviceName = !isPod ? resourceName : ''; const podName = isPod ? resourceName : pods[0].metadata.name; @@ -265,7 +265,7 @@ export default function PortForward(props: PortForwardProps) { }); } - if (isPod && (!resource || resource.status.phase === 'Failed')) { + if (isPod && (!resource || (resource as Pod).status.phase === 'Failed')) { return null; } diff --git a/frontend/src/components/common/Resource/Resource.tsx b/frontend/src/components/common/Resource/Resource.tsx index 1aec4051bf9..697b99af90f 100644 --- a/frontend/src/components/common/Resource/Resource.tsx +++ b/frontend/src/components/common/Resource/Resource.tsx @@ -13,7 +13,7 @@ import { useTheme } from '@mui/system'; import { Location } from 'history'; import { Base64 } from 'js-base64'; import _, { has } from 'lodash'; -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, NavLinkProps, useLocation } from 'react-router-dom'; import YAML from 'yaml'; @@ -24,8 +24,10 @@ import { KubeContainer, KubeContainerStatus, KubeObject, + KubeObjectClass, KubeObjectInterface, } from '../../../lib/k8s/cluster'; +import { KubeEvent } from '../../../lib/k8s/event'; import Pod, { KubePod, KubeVolume } from '../../../lib/k8s/pod'; import { createRouteURL, RouteURLProps } from '../../../lib/router'; import { getThemeName } from '../../../lib/themes'; @@ -60,7 +62,7 @@ export interface ResourceLinkProps extends Omit; } export function ResourceLink(props: ResourceLinkProps) { @@ -78,31 +80,31 @@ export function ResourceLink(props: ResourceLinkProps) { ); } -export interface DetailsGridProps - extends PropsWithChildren> { +export interface DetailsGridProps + extends PropsWithChildren>, 'resource'>> { /** Resource type to fetch (from the ResourceClasses). */ - resourceType: KubeObject; + resourceType: T; /** Name of the resource. */ name: string; /** Namespace of the resource. If not provided, it's assumed the resource is not namespaced. */ namespace?: string; /** Sections to show in the details grid (besides the default ones). */ extraSections?: - | ((item: KubeObject) => boolean | DetailsViewSection[]) + | ((item: InstanceType) => boolean | DetailsViewSection[] | ReactNode[]) | boolean | DetailsViewSection[]; /** @deprecated Use extraSections instead. */ - sectionsFunc?: (item: KubeObject) => React.ReactNode | DetailsViewSection[]; + sectionsFunc?: (item: InstanceType) => React.ReactNode | DetailsViewSection[]; /** If true, will show the events section. */ withEvents?: boolean; /** Called when the resource instance is created/updated, or there is an error. */ - onResourceUpdate?: (resource: KubeObject, error: ApiError) => void; + onResourceUpdate?: (resource: InstanceType, error: ApiError) => void; } /** Renders the different parts that constibute an actual resource's details view. * Those are: the back link, the header, the main info section, the extra sections, and the events section. */ -export function DetailsGrid(props: DetailsGridProps) { +export function DetailsGrid(props: DetailsGridProps) { const { sectionsFunc, resourceType, @@ -129,7 +131,7 @@ export function DetailsGrid(props: DetailsGridProps) { otherMainInfoSectionProps; const [item, error] = resourceType.useGet(name, namespace); - const prevItemRef = React.useRef<{ uid?: string; version?: string; error?: ApiError }>({}); + const prevItemRef = React.useRef<{ uid?: string; version?: string; error?: ApiError | null }>({}); React.useEffect(() => { if (item) { @@ -149,7 +151,7 @@ export function DetailsGrid(props: DetailsGridProps) { // infinite loops. const prevItem = prevItemRef.current; if ( - prevItem?.uid === item?.metatada?.uid && + prevItem?.uid === item?.metadata?.uid && prevItem?.version === item?.metadata?.resourceVersion && error === prevItem.error ) { @@ -157,11 +159,11 @@ export function DetailsGrid(props: DetailsGridProps) { } prevItemRef.current = { - uid: item?.metatada?.uid, + uid: item?.metadata?.uid, version: item?.metadata?.resourceVersion, error, }; - onResourceUpdate?.(item, error); + onResourceUpdate?.(item!, error!); }, [item, error]); const actualBackLink: string | Location | undefined = React.useMemo(() => { @@ -185,7 +187,7 @@ export function DetailsGrid(props: DetailsGridProps) { route = item.listRoute; } else { try { - route = new resourceType().listRoute; + route = new resourceType({} as any).listRoute; } catch (err) { console.error( `Error creating route for details grid (resource type=${resourceType}): ${err}` @@ -263,7 +265,7 @@ export function DetailsGrid(props: DetailsGridProps) { ); sections.push({ id: 'LEGACY_SECTIONS_FUNC', - section: sectionsFunc(item), + section: sectionsFunc(item!), }); } @@ -272,9 +274,9 @@ export function DetailsGrid(props: DetailsGridProps) { if (Array.isArray(extraSections)) { actualExtraSections = extraSections; } else if (typeof extraSections === 'function') { - const extraSectionsResult = extraSections(item) || []; + const extraSectionsResult = extraSections(item!) || []; if (Array.isArray(extraSectionsResult)) { - actualExtraSections = extraSectionsResult; + actualExtraSections = extraSectionsResult as any[]; } } @@ -493,7 +495,7 @@ export function SecretField(props: InputProps) { } export interface ConditionsTableProps { - resource: KubeObjectInterface | null; + resource: KubeObjectInterface | KubeEvent | null; showLastUpdate?: boolean; } @@ -636,7 +638,7 @@ export function LivenessProbes(props: { liveness: KubeContainer['livenessProbe'] export interface ContainerInfoProps { container: KubeContainer; - resource?: KubeObjectInterface | null; + resource: KubeObjectInterface | null; status?: Omit; } @@ -1141,7 +1143,7 @@ export function VolumeSection(props: VolumeSectionProps) { function PrintVolumeLink(props: PrintVolumeLinkProps) { const { volumeName, volumeKind, volume } = props; const resourceClasses = ResourceClasses; - const classList = Object.keys(resourceClasses); + const classList = Object.keys(resourceClasses) as Array; for (const kind of classList) { if (kind.toLowerCase() === volumeKind.toLowerCase()) { diff --git a/frontend/src/components/common/Resource/ResourceListView.tsx b/frontend/src/components/common/Resource/ResourceListView.tsx index 08cc7f5c2e3..03ab36784a6 100644 --- a/frontend/src/components/common/Resource/ResourceListView.tsx +++ b/frontend/src/components/common/Resource/ResourceListView.tsx @@ -1,28 +1,34 @@ import React, { PropsWithChildren } from 'react'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject, KubeObjectClass } from '../../../lib/k8s/cluster'; import SectionBox from '../SectionBox'; import SectionFilterHeader, { SectionFilterHeaderProps } from '../SectionFilterHeader'; import ResourceTable, { ResourceTableProps } from './ResourceTable'; -export interface ResourceListViewProps - extends PropsWithChildren> { +export interface ResourceListViewProps + extends PropsWithChildren, 'data'>> { title: string | JSX.Element; headerProps?: Omit; + data: Item[] | null; } -type Class = new (...args: any[]) => T; - -export interface ResourceListViewWithResourceClassProps - extends Omit, 'data'> { - resourceClass: Class; +export interface ResourceListViewWithResourceClassProps + extends PropsWithChildren>, 'data'>> { + title: string | JSX.Element; + headerProps?: Omit; + resourceClass: ItemClass; } -export default function ResourceListView( - props: ResourceListViewProps | ResourceListViewWithResourceClassProps +export default function ResourceListView( + props: ResourceListViewWithResourceClassProps +): any; +export default function ResourceListView>( + props: ResourceListViewProps +): any; +export default function ResourceListView( + props: ResourceListViewProps | ResourceListViewWithResourceClassProps ) { const { title, children, headerProps, ...tableProps } = props; - const withNamespaceFilter = - 'resourceClass' in props && (props.resourceClass as KubeObject)?.isNamespaced; + const withNamespaceFilter = 'resourceClass' in props && props.resourceClass?.isNamespaced; return ( ; + resourceTableArgs: ResourceTableFromResourceClassProps; namespaces: string[]; search: string; }> = args => { @@ -118,12 +118,12 @@ class MyPod extends Pod { ] as any; } -const podData: ResourceTableFromResourceClassProps = { +const podData: ResourceTableFromResourceClassProps = { columns: ['name', 'namespace', 'age'], resourceClass: MyPod, }; -const withHiddenCols: ResourceTableFromResourceClassProps = { +const withHiddenCols: ResourceTableFromResourceClassProps = { columns: [ 'name', 'namespace', diff --git a/frontend/src/components/common/Resource/ResourceTable.tsx b/frontend/src/components/common/Resource/ResourceTable.tsx index 2f5d8b55323..58124710cf2 100644 --- a/frontend/src/components/common/Resource/ResourceTable.tsx +++ b/frontend/src/components/common/Resource/ResourceTable.tsx @@ -4,7 +4,7 @@ import { MRT_FilterFns, MRT_Row, MRT_SortingFn } from 'material-react-table'; import { ComponentProps, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import helpers from '../../../helpers'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject, KubeObjectClass } from '../../../lib/k8s/cluster'; import { useFilterFunc } from '../../../lib/util'; import { HeadlampEventType, useEventCallback } from '../../../redux/headlampEventSlice'; import { useTypedSelector } from '../../../redux/reducers/reducers'; @@ -96,23 +96,28 @@ export interface ResourceTableProps { reflectInURL?: string | boolean; } -export interface ResourceTableFromResourceClassProps - extends Omit, 'data'> { - resourceClass: KubeObject; +export interface ResourceTableFromResourceClassProps + extends Omit>, 'data'> { + resourceClass: KubeClass; } -export default function ResourceTable( - props: ResourceTableFromResourceClassProps | ResourceTableProps +export default function ResourceTable( + props: + | ResourceTableFromResourceClassProps + | ResourceTableProps> ) { - if (!!(props as ResourceTableFromResourceClassProps).resourceClass) { - const { resourceClass, ...otherProps } = props as ResourceTableFromResourceClassProps; + if (!!(props as ResourceTableFromResourceClassProps).resourceClass) { + const { resourceClass, ...otherProps } = + props as ResourceTableFromResourceClassProps; return ; } - return )} />; + return >)} />; } -function TableFromResourceClass(props: ResourceTableFromResourceClassProps) { +function TableFromResourceClass( + props: ResourceTableFromResourceClassProps +) { const { resourceClass, id, ...otherProps } = props; const [items, error] = resourceClass.useList(); // throttle the update of the table to once per second @@ -121,7 +126,7 @@ function TableFromResourceClass(props: ResourceTableFromResourceClassPr useEffect(() => { dispatchHeadlampEvent({ - resources: items, + resources: items!, resourceKind: resourceClass.className, error: error || undefined, }); @@ -209,7 +214,7 @@ export function useThrottle(value: any, interval = 1000): any { return throttledValue; } -function ResourceTableContent(props: ResourceTableProps) { +function ResourceTableContent(props: ResourceTableProps) { const { columns, defaultSortingColumn, @@ -246,7 +251,7 @@ function ResourceTableContent(props: ResourceTableProps) { }); } const allColumns = processedColumns - .map((col, index): TableColumn => { + .map((col, index): TableColumn => { const indexId = String(index); if (typeof col !== 'string') { @@ -254,7 +259,7 @@ function ResourceTableContent(props: ResourceTableProps) { const sort = column.sort ?? true; - const mrtColumn: TableColumn = { + const mrtColumn: TableColumn = { id: column.id ?? indexId, header: column.label, filterVariant: column.filterVariant, @@ -276,7 +281,7 @@ function ResourceTableContent(props: ResourceTableProps) { } else if ('getter' in column) { mrtColumn.accessorFn = column.getter; } else { - mrtColumn.accessorFn = (item: KubeObject) => item[column.datum]; + mrtColumn.accessorFn = (item: RowItem) => item[column.datum]; } if ('render' in column) { mrtColumn.Cell = ({ row }: { row: MRT_Row }) => @@ -295,7 +300,7 @@ function ResourceTableContent(props: ResourceTableProps) { id: 'name', header: t('translation|Name'), gridTemplate: 1.5, - accessorFn: (item: KubeObject) => item.metadata.name, + accessorFn: (item: RowItem) => item.metadata.name, Cell: ({ row }: { row: MRT_Row }) => row.original && , }; @@ -304,13 +309,12 @@ function ResourceTableContent(props: ResourceTableProps) { id: 'age', header: t('translation|Age'), gridTemplate: 'min-content', - accessorFn: (item: KubeObject) => - -new Date(item.metadata.creationTimestamp).getTime(), + accessorFn: (item: RowItem) => -new Date(item.metadata.creationTimestamp).getTime(), enableColumnFilter: false, muiTableBodyCellProps: { align: 'right', }, - Cell: ({ row }: { row: MRT_Row }) => + Cell: ({ row }: { row: MRT_Row }) => row.original && ( (props: ResourceTableProps) { return { id: 'namespace', header: t('glossary|Namespace'), - accessorFn: (item: KubeObject) => item.getNamespace(), + accessorFn: (item: RowItem) => item.getNamespace(), filterVariant: 'multi-select', - Cell: ({ row }: { row: MRT_Row }) => + Cell: ({ row }: { row: MRT_Row }) => row.original?.getNamespace() ? ( {row.original.getNamespace()} @@ -339,7 +343,7 @@ function ResourceTableContent(props: ResourceTableProps) { return { id: 'kind', header: t('translation|Type'), - accessorFn: (resource: KubeObject) => String(resource?.kind), + accessorFn: (resource: RowItem) => String(resource?.kind), filterVariant: 'multi-select', }; default: @@ -347,7 +351,7 @@ function ResourceTableContent(props: ResourceTableProps) { } }) .filter(col => !hideColumns?.includes(col.id ?? '')) as Array< - TableColumn & { gridTemplate?: string | number } + TableColumn & { gridTemplate?: string | number } >; let sort = undefined; diff --git a/frontend/src/components/common/Resource/RestartButton.tsx b/frontend/src/components/common/Resource/RestartButton.tsx index 1b163ba1005..6a0a72936f0 100644 --- a/frontend/src/components/common/Resource/RestartButton.tsx +++ b/frontend/src/components/common/Resource/RestartButton.tsx @@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObject } from '../../../lib/k8s/cluster'; +import Deployment from '../../../lib/k8s/deployment'; import { clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, @@ -35,7 +36,7 @@ export function RestartButton(props: RestartButtonProps) { function applyFunc() { try { - const clonedItem = _.cloneDeep(item); + const clonedItem = _.cloneDeep(item) as Deployment; clonedItem.spec.template.metadata.annotations = { ...clonedItem.spec.template.metadata.annotations, 'kubectl.kubernetes.io/restartedAt': new Date().toISOString(), diff --git a/frontend/src/components/common/Resource/ScaleButton.tsx b/frontend/src/components/common/Resource/ScaleButton.tsx index 18010ef8700..eed7faa5f33 100644 --- a/frontend/src/components/common/Resource/ScaleButton.tsx +++ b/frontend/src/components/common/Resource/ScaleButton.tsx @@ -99,7 +99,7 @@ export default function ScaleButton(props: ScaleButtonProps) { ); } -interface ScaleDialogProps extends DialogProps { +interface ScaleDialogProps extends Omit { resource: KubeObject; onSave: (numReplicas: number) => void; onClose: () => void; @@ -127,11 +127,11 @@ function ScaleDialog(props: ScaleDialogProps) { const dispatchHeadlampEvent = useEventCallback(HeadlampEventType.SCALE_RESOURCE); function getNumReplicas() { - if (!resource?.spec) { + if (!('spec' in resource)) { return -1; } - return parseInt(resource.spec.replicas); + return parseInt((resource as any).spec.replicas); } const currentNumReplicas = getNumReplicas(); diff --git a/frontend/src/components/common/Resource/ViewButton.stories.tsx b/frontend/src/components/common/Resource/ViewButton.stories.tsx index b46a419773e..f9132720e29 100644 --- a/frontend/src/components/common/Resource/ViewButton.stories.tsx +++ b/frontend/src/components/common/Resource/ViewButton.stories.tsx @@ -1,6 +1,7 @@ import '../../../i18n/config'; import { Meta, Story } from '@storybook/react'; import React from 'react'; +import { KubeObject } from '../../../lib/k8s/cluster'; import ViewButton from './ViewButton'; import { ViewButtonProps } from './ViewButton'; @@ -16,13 +17,13 @@ export const View = Template.bind({}); View.args = { item: { jsonData: {}, - }, + } as KubeObject, }; export const ViewOpen = Template.bind({}); ViewOpen.args = { item: { jsonData: {}, - }, + } as KubeObject, initialToggle: true, }; diff --git a/frontend/src/components/common/SimpleTable.tsx b/frontend/src/components/common/SimpleTable.tsx index e43d50eb5d9..a0d57d5f950 100644 --- a/frontend/src/components/common/SimpleTable.tsx +++ b/frontend/src/components/common/SimpleTable.tsx @@ -43,7 +43,8 @@ export interface SimpleTableProps { [dataProp: string]: any; [dataProp: number]: any; }[] - | null; + | null + | undefined; filterFunction?: ((...args: any[]) => boolean) | null; rowsPerPage?: number[]; emptyMessage?: string; @@ -283,7 +284,7 @@ export default function SimpleTable(props: SimpleTableProps) { function getPagedRows() { const startIndex = page * rowsPerPage; - return filteredData.slice(startIndex, startIndex + rowsPerPage); + return filteredData!.slice(startIndex, startIndex + rowsPerPage); } if (displayData === null) { @@ -294,9 +295,9 @@ export default function SimpleTable(props: SimpleTableProps) { return ; } - let filteredData = displayData; + let filteredData = displayData!; if (filterFunction) { - filteredData = displayData.filter(filterFunction); + filteredData = displayData!.filter(filterFunction); } if ((filteredData.length === 0 || filteredData.length < page * rowsPerPage) && page !== 0) { @@ -415,7 +416,7 @@ export default function SimpleTable(props: SimpleTableProps) { )} - {filteredData.length > 0 ? ( + {filteredData!.length > 0 ? ( getPagedRows().map((row: any, i: number) => ( {columns.map((col, i) => { @@ -443,11 +444,11 @@ export default function SimpleTable(props: SimpleTableProps) { )} - {filteredData.length > rowsPerPageOptions[0] && showPagination && ( + {filteredData!.length > rowsPerPageOptions[0] && showPagination && ( { - const objList = [BASE_CONFIG_MAP, BASE_EMPTY_CONFIG_MAP].map( - (data: KubeObject) => new ConfigMap(data) - ); + const objList = [BASE_CONFIG_MAP, BASE_EMPTY_CONFIG_MAP].map(data => new ConfigMap(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/crd/CustomResourceDetails.stories.tsx b/frontend/src/components/crd/CustomResourceDetails.stories.tsx index 707a48eaba2..0f8ea9d0561 100644 --- a/frontend/src/components/crd/CustomResourceDetails.stories.tsx +++ b/frontend/src/components/crd/CustomResourceDetails.stories.tsx @@ -6,6 +6,7 @@ import { CustomResourceDetails, CustomResourceDetailsProps } from './CustomResou import { CRDMockMethods, CRMockClass } from './storyHelper'; // So we can test with a mocked CR. +// @ts-ignore ResourceClasses['mycustomresources'] = CRMockClass; export default { diff --git a/frontend/src/components/crd/CustomResourceDetails.tsx b/frontend/src/components/crd/CustomResourceDetails.tsx index a7ea28a3551..cd5190b04a2 100644 --- a/frontend/src/components/crd/CustomResourceDetails.tsx +++ b/frontend/src/components/crd/CustomResourceDetails.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { ResourceClasses } from '../../lib/k8s'; import { ApiError } from '../../lib/k8s/apiProxy'; +import { KubeObject } from '../../lib/k8s/cluster'; import CustomResourceDefinition, { KubeCRD } from '../../lib/k8s/crd'; import { localeDate } from '../../lib/util'; import { HoverInfoLabel, Link, NameValueTableRow, ObjectEventList, SectionBox } from '../common'; @@ -32,7 +33,7 @@ export function CustomResourceDetails(props: CustomResourceDetailsProps) { const { t } = useTranslation('glossary'); const namespace = ns === '-' ? undefined : ns; - const CRD = ResourceClasses.CustomResourceDefinition as CustomResourceDefinition; + const CRD = ResourceClasses.CustomResourceDefinition; CRD.useApiGet(setCRD, crdName, undefined, setError); @@ -109,7 +110,7 @@ export interface CustomResourceDetailsRendererProps { function CustomResourceDetailsRenderer(props: CustomResourceDetailsRendererProps) { const { crd, crName, namespace } = props; - const [item, setItem] = React.useState(null); + const [item, setItem] = React.useState(null); const [error, setError] = React.useState(null); const { t } = useTranslation('glossary'); @@ -152,7 +153,7 @@ function CustomResourceDetailsRenderer(props: CustomResourceDetailsRendererProps ), }, - ...getExtraInfo(extraColumns, item!.jsonData), + ...getExtraInfo(extraColumns, item!.jsonData as KubeCRD), ]} backLink="" /> diff --git a/frontend/src/components/crd/CustomResourceList.tsx b/frontend/src/components/crd/CustomResourceList.tsx index f63a64f78f5..5dd4a88c6d1 100644 --- a/frontend/src/components/crd/CustomResourceList.tsx +++ b/frontend/src/components/crd/CustomResourceList.tsx @@ -3,8 +3,8 @@ import { JSONPath } from 'jsonpath-plus'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { KubeObject } from '../../lib/k8s/cluster'; import CRD, { KubeCRD } from '../../lib/k8s/crd'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { localeDate } from '../../lib/util'; import { Link, Loader, PageGrid, SectionHeader } from '../common'; import BackLink from '../common/BackLink'; @@ -35,10 +35,14 @@ export default function CustomResourceList() { ); } - return ; + return ; } -function CustomResourceLink(props: { resource: KubeCRD; crd: CRD; [otherProps: string]: any }) { +function CustomResourceLink(props: { + resource: KubeObject; + crd: CRD; + [otherProps: string]: any; +}) { const { resource, crd, ...otherProps } = props; return ( @@ -83,7 +87,7 @@ function CustomResourceListRenderer(props: CustomResourceListProps) { ); } -function getValueWithJSONPath(item: KubeCRD, jsonPath: string): string { +function getValueWithJSONPath(item: { jsonData: object }, jsonPath: string): string { let value: string | undefined; try { // Extract the value from the json item @@ -110,7 +114,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { return crd.getMainAPIGroup(); }, [crd]); - const CRClass = React.useMemo(() => { + const CRClass: typeof KubeObject = React.useMemo(() => { return crd.makeCRClass(); }, [crd]); @@ -124,7 +128,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { crd.jsonData.spec.versions.find( (version: KubeCRD['spec']['versions'][number]) => version.name === currentVersion )?.additionalPrinterColumns || []; - const cols: ResourceTableColumn[] = []; + const cols: ResourceTableColumn>[] = []; for (let i = 0; i < colsFromSpec.length; i++) { const idx = i; const colSpec = colsFromSpec[idx]; @@ -150,15 +154,15 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { }, [crd, apiGroup]); const cols = React.useMemo(() => { - const colsToDisplay: ResourceTableProps['columns'] = [ + const colsToDisplay = [ { label: t('translation|Name'), getValue: resource => resource.metadata.name, - render: (resource: KubeObject) => , + render: resource => , }, ...additionalPrinterCols, 'age', - ]; + ] as ResourceTableProps>['columns']; if (crd.isNamespacedScope) { colsToDisplay.splice(1, 0, 'namespace'); diff --git a/frontend/src/components/crd/storyHelper.ts b/frontend/src/components/crd/storyHelper.ts index 0ad1c6ea43a..52cc4b6bdf9 100644 --- a/frontend/src/components/crd/storyHelper.ts +++ b/frontend/src/components/crd/storyHelper.ts @@ -1,6 +1,6 @@ import React from 'react'; import { ApiError, apiFactoryWithNamespace } from '../../lib/k8s/apiProxy'; -import { KubeObject, makeKubeObject } from '../../lib/k8s/cluster'; +import { KubeObject, KubeObjectInterface } from '../../lib/k8s/cluster'; import CustomResourceDefinition, { KubeCRD } from '../../lib/k8s/crd'; const mockCRDMap: { [crdName: string]: KubeCRD | null } = { @@ -45,7 +45,7 @@ const mockCRDMap: { [crdName: string]: KubeCRD | null } = { loadingcrd: null, }; -const mockCRMap: { [name: string]: KubeObject | null } = { +const mockCRMap: { [name: string]: KubeObjectInterface | null } = { mycustomresource_mynamespace: { kind: 'MyCustomResource', apiVersion: 'my.phonyresources.io/v1', @@ -100,14 +100,15 @@ const CRDMockMethods = { }, }; -class CRMockClass extends makeKubeObject('customresource') { +class CRMockClass extends KubeObject { + static objectName = 'customresource'; static apiEndpoint = apiFactoryWithNamespace(['', '', '']); static useApiGet( - setItem: (item: CRMockClass | null) => void, + setItem: (item: any | null) => void, // Update the type of the 'setItem' parameter name: string, namespace?: string, - setError?: (err: ApiError) => void + setError?: (err: ApiError | null) => void ) { React.useEffect(() => { const jsonData = mockCRMap[name + '_' + namespace]; @@ -123,7 +124,7 @@ class CRMockClass extends makeKubeObject('customresource') { static useApiList(onList: (...arg: any[]) => any) { React.useEffect(() => { - onList(Object.values(mockCRMap).map(cr => new CRMockClass(cr))); + onList(Object.values(mockCRMap).map(cr => new CRMockClass(cr!))); }, []); } diff --git a/frontend/src/components/cronjob/Details.tsx b/frontend/src/components/cronjob/Details.tsx index 28eed54c2f4..1907f7cc043 100644 --- a/frontend/src/components/cronjob/Details.tsx +++ b/frontend/src/components/cronjob/Details.tsx @@ -270,7 +270,7 @@ export default function CronJobDetails() { resourceType={CronJob} name={name} namespace={namespace} - onResourceUpdate={(cronJob: CronJob) => setCronJob(cronJob)} + onResourceUpdate={cronJob => setCronJob(cronJob)} withEvents actions={actions} extraInfo={item => diff --git a/frontend/src/components/endpoints/Details.tsx b/frontend/src/components/endpoints/Details.tsx index 030ded0bc9c..1a8614a4478 100644 --- a/frontend/src/components/endpoints/Details.tsx +++ b/frontend/src/components/endpoints/Details.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useParams } from 'react-router-dom'; import { ResourceClasses } from '../../lib/k8s'; -import Endpoints, { KubeEndpoint } from '../../lib/k8s/endpoints'; +import Endpoint, { KubeEndpoint } from '../../lib/k8s/endpoints'; import { Link, SectionHeader } from '../common'; import Empty from '../common/EmptyContent'; import { DetailsGrid } from '../common/Resource'; @@ -15,7 +15,7 @@ export default function EndpointDetails() { return ( { const targetRefClass = !!address.targetRef?.kind - ? ResourceClasses[address.targetRef?.kind] + ? ResourceClasses[ + address.targetRef?.kind as keyof typeof ResourceClasses + ] : null; if (!!targetRefClass) { return ( diff --git a/frontend/src/components/endpoints/EndpointDetails.stories.tsx b/frontend/src/components/endpoints/EndpointDetails.stories.tsx index 67f99d429f3..2da6237eefc 100644 --- a/frontend/src/components/endpoints/EndpointDetails.stories.tsx +++ b/frontend/src/components/endpoints/EndpointDetails.stories.tsx @@ -1,12 +1,12 @@ import { Meta, Story } from '@storybook/react'; import { KubeObjectClass } from '../../lib/k8s/cluster'; -import Endpoints, { KubeEndpoint } from '../../lib/k8s/endpoints'; +import Endpoint, { KubeEndpoint } from '../../lib/k8s/endpoints'; import { TestContext } from '../../test'; import EndpointDetails from './Details'; const usePhonyGet: KubeObjectClass['useGet'] = (name, namespace) => { return [ - new Endpoints({ + new Endpoint({ kind: 'Endpoints', apiVersion: 'v1', metadata: { @@ -83,10 +83,10 @@ interface MockerStory { const Template: Story = (args: MockerStory) => { if (!!args.useGet) { - Endpoints.useGet = args.useGet; + Endpoint.useGet = args.useGet; } if (!!args.useList) { - Endpoints.useList = args.useList; + Endpoint.useList = args.useList; } return ( diff --git a/frontend/src/components/endpoints/EndpointList.stories.tsx b/frontend/src/components/endpoints/EndpointList.stories.tsx index e85d606775c..4ebc18ce24a 100644 --- a/frontend/src/components/endpoints/EndpointList.stories.tsx +++ b/frontend/src/components/endpoints/EndpointList.stories.tsx @@ -1,16 +1,19 @@ import { Meta, Story } from '@storybook/react'; -import Endpoints, { KubeEndpoint } from '../../lib/k8s/endpoints'; +import Endpoint from '../../lib/k8s/endpoints'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import EndpointList from './List'; -Endpoints.useList = () => { - const objList = generateK8sResourceList( +Endpoint.useList = () => { + const objList = generateK8sResourceList( { kind: 'Endpoints', apiVersion: 'v1', metadata: { + name: '', namespace: '', + uid: '', + creationTimestamp: '', }, subsets: [ { @@ -38,7 +41,7 @@ Endpoints.useList = () => { }, ], }, - { instantiateAs: Endpoints } + { instantiateAs: Endpoint } ); return [objList, null, () => {}, () => {}] as any; diff --git a/frontend/src/components/horizontalPodAutoscaler/HPAList.stories.tsx b/frontend/src/components/horizontalPodAutoscaler/HPAList.stories.tsx index 8ac233be68d..3b1f917e441 100644 --- a/frontend/src/components/horizontalPodAutoscaler/HPAList.stories.tsx +++ b/frontend/src/components/horizontalPodAutoscaler/HPAList.stories.tsx @@ -1,5 +1,5 @@ import { Meta, Story } from '@storybook/react'; -import HPA, { KubeHPA } from '../../lib/k8s/hpa'; +import HPA from '../../lib/k8s/hpa'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import HpaList from './List'; @@ -11,7 +11,7 @@ HPA.getAuthorization = (): Promise<{ status: any }> => { }; HPA.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { apiVersion: 'autoscaling/v2', kind: 'HorizontalPodAutoscaler', diff --git a/frontend/src/components/ingress/ClassList.stories.tsx b/frontend/src/components/ingress/ClassList.stories.tsx index 28e963f3e5f..0b0ce92b134 100644 --- a/frontend/src/components/ingress/ClassList.stories.tsx +++ b/frontend/src/components/ingress/ClassList.stories.tsx @@ -1,5 +1,4 @@ import { Meta, Story } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import IngressClass from '../../lib/k8s/ingressClass'; import { TestContext } from '../../test'; import ListView from './ClassList'; @@ -7,7 +6,7 @@ import { RESOURCE_DEFAULT_INGRESS_CLASS, RESOURCE_INGRESS_CLASS } from './storyH IngressClass.useList = () => { const objList = [RESOURCE_INGRESS_CLASS, RESOURCE_DEFAULT_INGRESS_CLASS].map( - (data: KubeObject) => new IngressClass(data) + data => new IngressClass(data) ); return [objList, null, () => {}, () => {}] as any; diff --git a/frontend/src/components/ingress/Details.tsx b/frontend/src/components/ingress/Details.tsx index 249116c55f2..92325d01288 100644 --- a/frontend/src/components/ingress/Details.tsx +++ b/frontend/src/components/ingress/Details.tsx @@ -13,7 +13,7 @@ import SimpleTable from '../common/SimpleTable'; * Is https used in the ingress item */ function isHttpsUsed(item: Ingress, url: String) { - const hostList: string[] = item.jsonData.spec?.tls?.map(({ ...hosts }) => `${hosts.hosts}`); + const hostList = item.jsonData.spec?.tls?.map(({ ...hosts }) => `${hosts.hosts}`) ?? []; const isHttps = hostList.includes(`${url}`); return isHttps; diff --git a/frontend/src/components/ingress/List.stories.tsx b/frontend/src/components/ingress/List.stories.tsx index 0d0e13bbf4c..9d955620a72 100644 --- a/frontend/src/components/ingress/List.stories.tsx +++ b/frontend/src/components/ingress/List.stories.tsx @@ -1,12 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import Ingress from '../../lib/k8s/ingress'; import { TestContext } from '../../test'; import ListView from './List'; import { PORT_INGRESS, RESOURCE_INGRESS } from './storyHelper'; Ingress.useList = () => { - const objList = [PORT_INGRESS, RESOURCE_INGRESS].map((data: KubeObject) => new Ingress(data)); + const objList = [PORT_INGRESS, RESOURCE_INGRESS].map(data => new Ingress(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/job/storyHelper.ts b/frontend/src/components/job/storyHelper.ts index d09ae126120..433616847e6 100644 --- a/frontend/src/components/job/storyHelper.ts +++ b/frontend/src/components/job/storyHelper.ts @@ -1,8 +1,11 @@ -export const jobs = [ +import { KubeObjectInterface } from '../../lib/k8s/cluster'; + +export const jobs: KubeObjectInterface[] = [ { apiVersion: 'batch/v1', kind: 'Job', metadata: { + name: '', creationTimestamp: '2023-07-28T08:00:00Z', generation: 1, labels: { diff --git a/frontend/src/components/lease/List.stories.tsx b/frontend/src/components/lease/List.stories.tsx index f2c4d9a5e9e..d25d5c7f291 100644 --- a/frontend/src/components/lease/List.stories.tsx +++ b/frontend/src/components/lease/List.stories.tsx @@ -1,12 +1,11 @@ import { Meta, StoryFn } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import { Lease } from '../../lib/k8s/lease'; import { TestContext } from '../../test'; import { LeaseList } from './List'; import { LEASE_DUMMY_DATA } from './storyHelper'; Lease.useList = () => { - const objList = LEASE_DUMMY_DATA.map((data: KubeObject) => new Lease(data)); + const objList = LEASE_DUMMY_DATA.map(data => new Lease(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/limitRange/List.stories.tsx b/frontend/src/components/limitRange/List.stories.tsx index fbad7388b25..afb4e0734a3 100644 --- a/frontend/src/components/limitRange/List.stories.tsx +++ b/frontend/src/components/limitRange/List.stories.tsx @@ -1,12 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import { LimitRange } from '../../lib/k8s/limitRange'; import { TestContext } from '../../test'; import { LimitRangeList } from './List'; import { LIMIT_RANGE_DUMMY_DATA } from './storyHelper'; LimitRange.useList = () => { - const objList = LIMIT_RANGE_DUMMY_DATA.map((data: KubeObject) => new LimitRange(data)); + const objList = LIMIT_RANGE_DUMMY_DATA.map(data => new LimitRange(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/namespace/List.tsx b/frontend/src/components/namespace/List.tsx index cea0403bb49..f8568fe2eff 100644 --- a/frontend/src/components/namespace/List.tsx +++ b/frontend/src/components/namespace/List.tsx @@ -39,8 +39,8 @@ export default function NamespacesList() { } const resourceTableProps: - | ResourceTableFromResourceClassProps - | ResourceTableProps = React.useMemo(() => { + | ResourceTableProps + | ResourceTableFromResourceClassProps = React.useMemo(() => { if (allowedNamespaces.length > 0) { return { columns: [ @@ -71,7 +71,7 @@ export default function NamespacesList() { }, ], data: allowedNamespaces as unknown as Namespace[], - }; + } satisfies ResourceTableProps; } return { resourceClass: Namespace, @@ -85,7 +85,7 @@ export default function NamespacesList() { }, 'age', ], - }; + } satisfies ResourceTableFromResourceClassProps; }, [allowedNamespaces]); return ( @@ -95,7 +95,7 @@ export default function NamespacesList() { titleSideActions: [], noNamespaceFilter: true, }} - {...resourceTableProps} + {...(resourceTableProps as ResourceTableProps)} /> ); } diff --git a/frontend/src/components/namespace/NamespaceList.stories.tsx b/frontend/src/components/namespace/NamespaceList.stories.tsx index 93cda719134..386ecc87b24 100644 --- a/frontend/src/components/namespace/NamespaceList.stories.tsx +++ b/frontend/src/components/namespace/NamespaceList.stories.tsx @@ -1,15 +1,16 @@ import { Meta, Story } from '@storybook/react'; -import Namespace, { KubeNamespace } from '../../lib/k8s/namespace'; +import { KubeMetadata } from '../../lib/k8s/cluster'; +import Namespace from '../../lib/k8s/namespace'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import NamespacesList from './List'; Namespace.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { kind: 'Namespace', apiVersion: 'v1', - metadata: {}, + metadata: {} as KubeMetadata, spec: { finalizers: ['kubernetes'], }, diff --git a/frontend/src/components/node/Details.tsx b/frontend/src/components/node/Details.tsx index 053110f9ec5..c6facae7b1e 100644 --- a/frontend/src/components/node/Details.tsx +++ b/frontend/src/components/node/Details.tsx @@ -113,7 +113,7 @@ export default function NodeDetails() { } const cloneNode = _.cloneDeep(node); - cloneNode.spec.unschedulable = !node.spec.unschedulable; + cloneNode!.spec.unschedulable = !node!.spec.unschedulable; setNode(cloneNode); }) .catch(error => { @@ -167,7 +167,7 @@ export default function NodeDetails() { })} onConfirm={() => { setDrainDialogOpen(false); - handleNodeDrain(node); + handleNodeDrain(node!); }} handleClose={() => setDrainDialogOpen(false)} open={drainDialogOpen} @@ -199,7 +199,7 @@ export default function NodeDetails() { handleNodeScheduleState(item, cordon)} + onClick={() => handleNodeScheduleState(item!, cordon)} iconButtonProps={{ disabled: isupdatingNodeScheduleProperty, }} diff --git a/frontend/src/components/node/List.tsx b/frontend/src/components/node/List.tsx index a495c644c9e..ef0fbdbbfb2 100644 --- a/frontend/src/components/node/List.tsx +++ b/frontend/src/components/node/List.tsx @@ -78,7 +78,7 @@ export default function NodeList() { label: t('Roles'), gridTemplate: 'minmax(150px, .5fr)', getValue: node => { - return Object.keys(node.metadata.labels) + return Object.keys(node.metadata.labels ?? {}) .filter((t: String) => t.startsWith('node-role.kubernetes.io/')) .map(t => t.replace('node-role.kubernetes.io/', '')) .join(','); diff --git a/frontend/src/components/pod/List.tsx b/frontend/src/components/pod/List.tsx index d7a1a2d7800..fcf433b166e 100644 --- a/frontend/src/components/pod/List.tsx +++ b/frontend/src/components/pod/List.tsx @@ -90,7 +90,7 @@ export function PodListRenderer(props: PodListProps) { 'namespace', { label: t('Restarts'), - getValue: (pod: Pod) => { + getValue: pod => { const { restarts, lastRestartDate } = pod.getDetailedStatus(); return lastRestartDate.getTime() !== 0 ? t('{{ restarts }} ({{ abbrevTime }} ago)', { @@ -103,7 +103,7 @@ export function PodListRenderer(props: PodListProps) { { id: 'ready', label: t('translation|Ready'), - getValue: (pod: Pod) => { + getValue: pod => { const podRow = pod.getDetailedStatus(); return `${podRow.readyContainers}/${podRow.totalContainers}`; }, @@ -117,7 +117,7 @@ export function PodListRenderer(props: PodListProps) { { id: 'ip', label: t('glossary|IP'), - getValue: (pod: Pod) => pod.status?.podIP ?? '', + getValue: pod => pod.status?.podIP ?? '', }, { id: 'node', @@ -159,7 +159,7 @@ export function PodListRenderer(props: PodListProps) { return statusTrueCount; }, - render: (pod: Pod) => { + render: pod => { const readinessGatesStatus = getReadinessGatesStatus(pod); const total = Object.keys(readinessGatesStatus).length; @@ -218,7 +218,7 @@ export default function PodList() { React.useEffect(() => { dispatchHeadlampEvent({ - resources: pods, + resources: pods!, resourceKind: 'Pod', error: error || undefined, }); diff --git a/frontend/src/components/pod/PodLogs.stories.tsx b/frontend/src/components/pod/PodLogs.stories.tsx index 5218c3983a4..e2d88366329 100644 --- a/frontend/src/components/pod/PodLogs.stories.tsx +++ b/frontend/src/components/pod/PodLogs.stories.tsx @@ -48,7 +48,7 @@ export default { interface MockerStory { podName: string; detailsProps: PodDetailsProps; - [key: string]: Pod[keyof typeof Pod | keyof typeof Pod.prototype]; + [key: string]: Pod[keyof Pod | keyof typeof Pod.prototype]; } const Template: Story = args => { @@ -57,11 +57,11 @@ const Template: Story = args => { for (const key in podProps) { const [prefix, method] = key.split('.'); if (prefix === 'prototype') { - Pod.prototype[method as keyof typeof Pod.prototype] = podProps[key]; + (Pod as any).prototype[method] = podProps[key]; continue; } - Pod[key as keyof typeof Pod] = podProps[key]; + (Pod as any)[key] = podProps[key]; } return ( diff --git a/frontend/src/components/pod/storyHelper.ts b/frontend/src/components/pod/storyHelper.ts index 49f4304cadc..b4dccc3a48e 100644 --- a/frontend/src/components/pod/storyHelper.ts +++ b/frontend/src/components/pod/storyHelper.ts @@ -740,7 +740,7 @@ const readinessGate = { }; // Exporting so these can be used for details views -export const podList = [ +export const podList: KubePod[] = [ imgPullBackOff, successful, initializing, diff --git a/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx b/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx index bd01c3b1948..0afb6ec1d85 100644 --- a/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx +++ b/frontend/src/components/podDisruptionBudget/pdbList.stories.tsx @@ -1,11 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import PDB, { KubePDB } from '../../lib/k8s/podDisruptionBudget'; +import PDB from '../../lib/k8s/podDisruptionBudget'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import PDBList from './List'; PDB.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { kind: 'PodDisruptionBudget', metadata: { diff --git a/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx b/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx index 7a6e11a0330..4b30c1a17d5 100644 --- a/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx +++ b/frontend/src/components/priorityClass/priorityClassDetails.stories.tsx @@ -5,7 +5,7 @@ import PriorityClass, { KubePriorityClass } from '../../lib/k8s/priorityClass'; import { TestContext } from '../../test'; import HPADetails from './Details'; -const usePhonyGet: KubeObjectClass['useGet'] = () => { +const usePhonyGet = () => { return [ new PriorityClass({ description: 'Mission Critical apps.', diff --git a/frontend/src/components/priorityClass/priorityClassList.stories.tsx b/frontend/src/components/priorityClass/priorityClassList.stories.tsx index 18ac92d1858..741309bc429 100644 --- a/frontend/src/components/priorityClass/priorityClassList.stories.tsx +++ b/frontend/src/components/priorityClass/priorityClassList.stories.tsx @@ -1,11 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import PriorityClass, { KubePriorityClass } from '../../lib/k8s/priorityClass'; +import PriorityClass from '../../lib/k8s/priorityClass'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import PriorityClassList from './List'; PriorityClass.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { description: 'Mission Critical apps.', kind: 'PriorityClass', diff --git a/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx b/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx index 067996a6e36..f38a4df07c7 100644 --- a/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx +++ b/frontend/src/components/resourceQuota/resourceQuotaList.stories.tsx @@ -1,11 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import ResourceQuota, { KubeResourceQuota } from '../../lib/k8s/resourceQuota'; +import ResourceQuota from '../../lib/k8s/resourceQuota'; import { TestContext } from '../../test'; import { generateK8sResourceList } from '../../test/mocker'; import ResourceQuotaList from './List'; ResourceQuota.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { apiVersion: 'v1', kind: 'ResourceQuota', diff --git a/frontend/src/components/runtimeClass/List.stories.tsx b/frontend/src/components/runtimeClass/List.stories.tsx index c3bdba0be13..e46057cb620 100644 --- a/frontend/src/components/runtimeClass/List.stories.tsx +++ b/frontend/src/components/runtimeClass/List.stories.tsx @@ -1,12 +1,11 @@ import { Meta, StoryFn } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import { RuntimeClass } from '../../lib/k8s/runtime'; import { TestContext } from '../../test'; import { RuntimeClassList } from './List'; import { RUNTIME_CLASS_DUMMY_DATA } from './storyHelper'; RuntimeClass.useList = () => { - const objList = RUNTIME_CLASS_DUMMY_DATA.map((data: KubeObject) => new RuntimeClass(data)); + const objList = RUNTIME_CLASS_DUMMY_DATA.map(data => new RuntimeClass(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/secret/List.stories.tsx b/frontend/src/components/secret/List.stories.tsx index c93ae63a3fe..c1f83e84b0c 100644 --- a/frontend/src/components/secret/List.stories.tsx +++ b/frontend/src/components/secret/List.stories.tsx @@ -1,12 +1,11 @@ import { Meta, StoryFn } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import Secret from '../../lib/k8s/secret'; import { TestContext } from '../../test'; import ListView from './List'; import { BASE_EMPTY_SECRET, BASE_SECRET } from './storyHelper'; Secret.useList = () => { - const objList = [BASE_EMPTY_SECRET, BASE_SECRET].map((data: KubeObject) => new Secret(data)); + const objList = [BASE_EMPTY_SECRET, BASE_SECRET].map(data => new Secret(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx index 2c260aa8ad1..ea568c7c328 100644 --- a/frontend/src/components/service/Details.tsx +++ b/frontend/src/components/service/Details.tsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import Endpoints from '../../lib/k8s/endpoints'; +import Endpoint from '../../lib/k8s/endpoints'; import Service from '../../lib/k8s/service'; import { Link } from '../common'; import Empty from '../common/EmptyContent'; @@ -18,7 +18,7 @@ export default function ServiceDetails() { const { namespace, name } = useParams<{ namespace: string; name: string }>(); const { t } = useTranslation(['glossary', 'translation']); - const [endpoints, endpointsError] = Endpoints.useList({ namespace }); + const [endpoints, endpointsError] = Endpoint.useList({ namespace }); function getOwnedEndpoints(item: Service) { return item ? endpoints?.filter(endpoint => endpoint.getName() === item.getName()) : null; diff --git a/frontend/src/components/serviceaccount/Details.tsx b/frontend/src/components/serviceaccount/Details.tsx index f89f65716da..5b1ebf4c760 100644 --- a/frontend/src/components/serviceaccount/Details.tsx +++ b/frontend/src/components/serviceaccount/Details.tsx @@ -15,7 +15,7 @@ export default function ServiceAccountDetails() { name={name} namespace={namespace} withEvents - extraInfo={(item: ServiceAccount) => + extraInfo={item => item && [ { name: t('Secrets'), diff --git a/frontend/src/components/storage/ClaimDetails.tsx b/frontend/src/components/storage/ClaimDetails.tsx index 46ae649b291..5dc95c8b092 100644 --- a/frontend/src/components/storage/ClaimDetails.tsx +++ b/frontend/src/components/storage/ClaimDetails.tsx @@ -7,7 +7,7 @@ import { StatusLabelByPhase } from './utils'; export function makePVCStatusLabel(item: PersistentVolumeClaim) { const status = item.status!.phase; - return StatusLabelByPhase(status); + return StatusLabelByPhase(status!); } export default function VolumeClaimDetails() { @@ -28,11 +28,11 @@ export default function VolumeClaimDetails() { }, { name: t('Capacity'), - value: item.spec!.resources.requests.storage, + value: item.spec!.resources!.requests.storage, }, { name: t('Access Modes'), - value: item.spec!.accessModes.join(', '), + value: item.spec!.accessModes!.join(', '), }, { name: t('Volume Mode'), diff --git a/frontend/src/components/storage/ClaimList.tsx b/frontend/src/components/storage/ClaimList.tsx index d4552414b25..86512f2e779 100644 --- a/frontend/src/components/storage/ClaimList.tsx +++ b/frontend/src/components/storage/ClaimList.tsx @@ -18,9 +18,9 @@ export default function VolumeClaimList() { { id: 'className', label: t('Class Name'), - getValue: volumeClaim => volumeClaim.spec.storageClassName, + getValue: volumeClaim => volumeClaim.spec?.storageClassName, render: volumeClaim => { - const name = volumeClaim.spec.storageClassName; + const name = volumeClaim.spec?.storageClassName; if (!name) { return ''; } @@ -34,26 +34,26 @@ export default function VolumeClaimList() { { id: 'capacity', label: t('Capacity'), - getValue: volumeClaim => volumeClaim.status.capacity?.storage, + getValue: volumeClaim => volumeClaim.status?.capacity?.storage, gridTemplate: 0.8, }, { id: 'accessModes', label: t('Access Modes'), - getValue: volumeClaim => volumeClaim.spec.accessModes.join(', '), - render: volumeClaim => , + getValue: volumeClaim => volumeClaim.spec?.accessModes?.join(', '), + render: volumeClaim => , }, { id: 'volumeMode', label: t('Volume Mode'), - getValue: volumeClaim => volumeClaim.spec.volumeMode, + getValue: volumeClaim => volumeClaim.spec?.volumeMode, }, { id: 'volume', label: t('Volume'), - getValue: volumeClaim => volumeClaim.spec.volumeName, + getValue: volumeClaim => volumeClaim.spec?.volumeName, render: volumeClaim => { - const name = volumeClaim.spec.volumeName; + const name = volumeClaim.spec?.volumeName; if (!name) { return ''; } @@ -67,7 +67,7 @@ export default function VolumeClaimList() { { id: 'status', label: t('translation|Status'), - getValue: volume => volume.status.phase, + getValue: volume => volume.status?.phase, render: volume => makePVCStatusLabel(volume), gridTemplate: 0.3, }, diff --git a/frontend/src/components/storage/ClassList.stories.tsx b/frontend/src/components/storage/ClassList.stories.tsx index f084870815a..1dcf2d1c544 100644 --- a/frontend/src/components/storage/ClassList.stories.tsx +++ b/frontend/src/components/storage/ClassList.stories.tsx @@ -1,12 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import StorageClass from '../../lib/k8s/storageClass'; import { TestContext } from '../../test'; import ListView from './ClassList'; import { BASE_SC } from './storyHelper'; StorageClass.useList = () => { - const objList = [BASE_SC].map((data: KubeObject) => new StorageClass(data)); + const objList = [BASE_SC].map(data => new StorageClass(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/storage/ClassList.tsx b/frontend/src/components/storage/ClassList.tsx index d9c07df2ce2..d12a9301214 100644 --- a/frontend/src/components/storage/ClassList.tsx +++ b/frontend/src/components/storage/ClassList.tsx @@ -32,7 +32,7 @@ export default function ClassList() { { id: 'allowVolumeExpansion', label: t('Allow Volume Expansion'), - getValue: storageClass => storageClass.allowVolumeExpansion, + getValue: storageClass => String(storageClass.allowVolumeExpansion), }, 'age', ]} diff --git a/frontend/src/components/storage/VolumeList.stories.tsx b/frontend/src/components/storage/VolumeList.stories.tsx index 58b15a53a1b..7e454dbd31f 100644 --- a/frontend/src/components/storage/VolumeList.stories.tsx +++ b/frontend/src/components/storage/VolumeList.stories.tsx @@ -1,12 +1,11 @@ import { Meta, Story } from '@storybook/react'; -import { KubeObject } from '../../lib/k8s/cluster'; import PersistentVolume from '../../lib/k8s/persistentVolume'; import { TestContext } from '../../test'; import ListView from './ClassList'; import { BASE_PV } from './storyHelper'; PersistentVolume.useList = () => { - const objList = [BASE_PV].map((data: KubeObject) => new PersistentVolume(data)); + const objList = [BASE_PV].map(data => new PersistentVolume(data)); return [objList, null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot index 743b2029abe..fd827b4150f 100644 --- a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot +++ b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot @@ -667,7 +667,9 @@ + > + true + + > + true + Promise.resolve(true); VPA.useList = () => { - const objList = generateK8sResourceList( + const objList = generateK8sResourceList( { apiVersion: 'autoscaling.k8s.io/v1', kind: 'VerticalPodAutoscaler', diff --git a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx index e618b9fab92..519595866a4 100644 --- a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx +++ b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx @@ -1,10 +1,10 @@ import { Meta, Story } from '@storybook/react'; -import MWC, { KubeMutatingWebhookConfiguration } from '../../lib/k8s/mutatingWebhookConfiguration'; +import MWC from '../../lib/k8s/mutatingWebhookConfiguration'; import { TestContext } from '../../test'; import MutatingWebhookConfigDetails from './MutatingWebhookConfigDetails'; import { createMWC } from './storyHelper'; -const usePhonyGet: KubeMutatingWebhookConfiguration['useGet'] = (withService: boolean) => { +const usePhonyGet = (withService: boolean) => { return [new MWC(createMWC(withService)), null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx index da6a6bbba5b..6457c49aa20 100644 --- a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx +++ b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx @@ -1,12 +1,10 @@ import { Meta, Story } from '@storybook/react'; -import VWC, { - KubeValidatingWebhookConfiguration, -} from '../../lib/k8s/validatingWebhookConfiguration'; +import VWC from '../../lib/k8s/validatingWebhookConfiguration'; import { TestContext } from '../../test'; import { createVWC } from './storyHelper'; import ValidatingWebhookConfigDetails from './ValidatingWebhookConfigDetails'; -const usePhonyGet: KubeValidatingWebhookConfiguration['useGet'] = (withService: boolean) => { +const usePhonyGet = (withService: boolean) => { return [new VWC(createVWC(withService)), null, () => {}, () => {}] as any; }; diff --git a/frontend/src/components/workload/Details.tsx b/frontend/src/components/workload/Details.tsx index 103a7781dd1..2ebe656ebce 100644 --- a/frontend/src/components/workload/Details.tsx +++ b/frontend/src/components/workload/Details.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { KubeObject, Workload } from '../../lib/k8s/cluster'; +import { Workload, WorkloadClass } from '../../lib/k8s/cluster'; import { ConditionsSection, ContainersSection, @@ -9,11 +9,11 @@ import { OwnedPodsSection, } from '../common/Resource'; -interface WorkloadDetailsProps { - workloadKind: KubeObject; +interface WorkloadDetailsProps { + workloadKind: T; } -export default function WorkloadDetails(props: WorkloadDetailsProps) { +export default function WorkloadDetails(props: WorkloadDetailsProps) { const { namespace, name } = useParams<{ namespace: string; name: string }>(); const { workloadKind } = props; const { t } = useTranslation(['glossary', 'translation']); diff --git a/frontend/src/components/workload/Overview.tsx b/frontend/src/components/workload/Overview.tsx index 84bcefd1c7e..d045b124a53 100644 --- a/frontend/src/components/workload/Overview.tsx +++ b/frontend/src/components/workload/Overview.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { useCluster } from '../../lib/k8s'; import { ApiError } from '../../lib/k8s/apiProxy'; -import { KubeObject, Workload } from '../../lib/k8s/cluster'; +import { Workload, WorkloadClass } from '../../lib/k8s/cluster'; import CronJob from '../../lib/k8s/cronJob'; import DaemonSet from '../../lib/k8s/daemonSet'; import Deployment from '../../lib/k8s/deployment'; @@ -72,7 +72,7 @@ export default function Overview() { return joint; } - const workloads: KubeObject[] = [ + const workloads: WorkloadClass[] = [ Pod, Deployment, StatefulSet, @@ -82,9 +82,9 @@ export default function Overview() { CronJob, ]; - workloads.forEach((workloadClass: KubeObject) => { + workloads.forEach(workloadClass => { workloadClass.useApiList( - (items: InstanceType[]) => { + (items: Workload[]) => { setWorkloads({ [workloadClass.className]: items }); }, (err: ApiError) => { @@ -94,7 +94,7 @@ export default function Overview() { ); }); - function ChartLink(workload: KubeObject) { + function ChartLink(workload: WorkloadClass) { const linkName = workload.pluralName; return {linkName}; } @@ -103,7 +103,7 @@ export default function Overview() { - {workloads.map(workload => ( + {workloads.map((workload: WorkloadClass) => ( item.metadata.name, - render: item => ( - - ), + render: item => , }, 'namespace', { diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts new file mode 100644 index 00000000000..1532a5b6b9f --- /dev/null +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -0,0 +1,574 @@ +import { OpPatch } from 'json-patch'; +import { JSONPath } from 'jsonpath-plus'; +import { cloneDeep, unset } from 'lodash'; +import React from 'react'; +import helpers from '../../helpers'; +import { getCluster } from '../cluster'; +import { createRouteURL } from '../router'; +import { timeAgo, useErrorState } from '../util'; +import { useCluster, useConnectApi } from '.'; +import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy'; +import { + ApiListOptions, + ApiListSingleNamespaceOptions, + AuthRequestResourceAttrs, + KubeMetadata, + KubeObjectClass, + KubeObjectInterface, +} from './cluster'; +import { KubeEvent } from './event'; + +function getAllowedNamespaces() { + const cluster = getCluster(); + if (!cluster) { + return []; + } + + const clusterSettings = helpers.loadClusterSettings(cluster); + return clusterSettings.allowedNamespaces || []; +} + +export class KubeObject { + static apiEndpoint: ReturnType; + static readOnlyFields: string[] = []; + static objectName: string; + + jsonData: T; + readonly _clusterName: string; + + constructor(json: T) { + this.jsonData = json; + this._clusterName = getCluster() || ''; + } + + static get className(): string { + return this.objectName; + } + + get detailsRoute(): string { + return this._class().detailsRoute; + } + + static get detailsRoute(): string { + return this.className; + } + + static get pluralName(): string { + // This is a naive way to get the plural name of the object by default. It will + // work in most cases, but for exceptions (like Ingress), we must override this. + return this.className.toLowerCase() + 's'; + } + + get pluralName(): string { + // In case we need to override the plural name in instances. + return this._class().pluralName; + } + + get listRoute(): string { + return this._class().listRoute; + } + + static get listRoute(): string { + return this.detailsRoute + 's'; + } + + getDetailsLink() { + const params = { + namespace: this.getNamespace(), + name: this.getName(), + }; + const link = createRouteURL(this.detailsRoute, params); + return link; + } + + getListLink() { + return createRouteURL(this.listRoute); + } + + getName() { + return this.metadata.name; + } + + getNamespace() { + return this.metadata.namespace; + } + + getCreationTs() { + return this.metadata.creationTimestamp; + } + + getAge() { + return timeAgo(this.getCreationTs()); + } + + getValue(prop: string) { + return (this.jsonData as Record)![prop]; + } + + get metadata() { + return this.jsonData.metadata; + } + + get kind() { + return this.jsonData.kind; + } + + get isNamespaced() { + return this._class().isNamespaced; + } + + static get isNamespaced() { + return this.apiEndpoint.isNamespaced; + } + + getEditableObject() { + const fieldsToRemove = this._class().readOnlyFields; + const code = this.jsonData ? cloneDeep(this.jsonData) : {}; + + fieldsToRemove?.forEach(path => { + JSONPath({ + path, + json: code, + callback: (result, type, fullPayload) => { + if (fullPayload.parent && fullPayload.parentProperty) { + delete fullPayload.parent[fullPayload.parentProperty]; + } + }, + resultType: 'all', + }); + }); + + return code; + } + + // @todo: apiList has 'any' return type. + /** + * Returns the API endpoint for this object. + * + * @param onList - Callback function to be called when the list is retrieved. + * @param onError - Callback function to be called when an error occurs. + * @param opts - Options to be passed to the API endpoint. + * + * @returns The API endpoint for this object. + */ + static apiList( + this: U, + onList: (arg: InstanceType[]) => void, + onError?: (err: ApiError) => void, + opts?: ApiListSingleNamespaceOptions + ) { + const createInstance = (item: any): any => this.create(item); + + const args: any[] = [(list: any[]) => onList(list.map((item: any) => createInstance(item)))]; + + if (this.apiEndpoint.isNamespaced) { + args.unshift(opts?.namespace || null); + } + + args.push(onError); + + const queryParams: QueryParameters = {}; + if (opts?.queryParams?.labelSelector) { + queryParams['labelSelector'] = opts.queryParams.labelSelector; + } + if (opts?.queryParams?.fieldSelector) { + queryParams['fieldSelector'] = opts.queryParams.fieldSelector; + } + if (opts?.queryParams?.limit) { + queryParams['limit'] = opts.queryParams.limit; + } + args.push(queryParams); + + args.push(opts?.cluster); + + return this.apiEndpoint.list.bind(null, ...args); + } + + static useApiList( + this: U, + onList: (...arg: any[]) => any, + onError?: (err: ApiError) => void, + opts?: ApiListOptions + ) { + const [objs, setObjs] = React.useState<{ [key: string]: U[] }>({}); + const listCallback = onList as (arg: any[]) => void; + + function onObjs(namespace: string, objList: U[]) { + let newObjs: typeof objs = {}; + // Set the objects so we have them for the next API response... + setObjs(previousObjs => { + newObjs = { ...previousObjs, [namespace || '']: objList }; + return newObjs; + }); + + let allObjs: U[] = []; + Object.values(newObjs).map(currentObjs => { + allObjs = allObjs.concat(currentObjs); + }); + + listCallback(allObjs); + } + + const listCalls = []; + const queryParams = cloneDeep(opts); + let namespaces: string[] = []; + unset(queryParams, 'namespace'); + + const cluster = opts?.cluster; + + if (!!opts?.namespace) { + if (typeof opts.namespace === 'string') { + namespaces = [opts.namespace]; + } else if (Array.isArray(opts.namespace)) { + namespaces = opts.namespace as string[]; + } else { + throw Error('namespace should be a string or array of strings'); + } + } + + // If the request itself has no namespaces set, we check whether to apply the + // allowed namespaces. + if (namespaces.length === 0 && this.isNamespaced) { + namespaces = getAllowedNamespaces(); + } + + if (namespaces.length > 0) { + // If we have a namespace set, then we have to make an API call for each + // namespace and then set the objects once we have all of the responses. + for (const namespace of namespaces) { + listCalls.push( + this.apiList(objList => onObjs(namespace, objList as U[]), onError, { + namespace, + queryParams, + cluster, + }) + ); + } + } else { + // If we don't have a namespace set, then we only have one API call + // response to set and we return it right away. + listCalls.push(this.apiList(listCallback, onError, { queryParams, cluster })); + } + + useConnectApi(...listCalls); + } + + static useList( + this: U, + opts?: ApiListOptions + ): [ + InstanceType[] | null, + ApiError | null, + (items: InstanceType[]) => void, + (err: ApiError | null) => void + ] { + const [objList, setObjList] = React.useState[] | null>(null); + const [error, setError] = useErrorState(setObjList); + const currentCluster = useCluster(); + const cluster = opts?.cluster || currentCluster; + + // Reset the list and error when the cluster changes. + React.useEffect(() => { + setObjList(null); + setError(null); + }, [cluster]); + + function setList(items: InstanceType[] | null) { + setObjList(items); + if (items !== null) { + setError(null); + } + } + + this.useApiList(setList, setError, opts); + + // Return getters and then the setters as the getters are more likely to be used with + // this function. + return [objList, error, setObjList, setError]; + } + + static create>(this: T, item: ConstructorParameters[0]) { + return new this(item) as InstanceType; + } + + static apiGet( + this: U, + onGet: (...args: any) => void, + name: string, + namespace?: string, + onError?: (err: ApiError | null) => void, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ) { + const createInstance = (item: any) => this.create(item); + const args: any[] = [name, (obj: any) => onGet(createInstance(obj))]; + + if (this.apiEndpoint.isNamespaced) { + args.unshift(namespace); + } + + args.push(onError); + args.push(opts?.queryParams); + args.push(opts?.cluster); + + return this.apiEndpoint.get.bind(null, ...args); + } + + static useApiGet( + this: U, + onGet: (item: InstanceType | null) => any, + name: string, + namespace?: string, + onError?: (err: ApiError | null) => void, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ) { + // We do the type conversion here because we want to be able to use hooks that may not have + // the exact signature as get callbacks. + const getCallback = onGet as (item: U) => void; + useConnectApi(this.apiGet(getCallback, name, namespace, onError, opts)); + } + + static useGet( + this: U, + name: string, + namespace?: string, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ): [ + InstanceType | null, + ApiError | null, + (items: InstanceType) => void, + (err: ApiError | null) => void + ] { + const [obj, setObj] = React.useState | null>(null); + const [error, setError] = useErrorState(setObj); + + function onGet(item: InstanceType | null) { + // Only set the object if we have we have a different one. + if (!!obj && !!item && obj.metadata.resourceVersion === item.metadata.resourceVersion) { + return; + } + + setObj(item); + if (item !== null) { + setError(null); + } + } + + function onError(err: ApiError | null) { + if ( + error === err || + (!!error && !!err && error.message === err.message && error.status === err.status) + ) { + return; + } + + setError(err); + } + + this.useApiGet(onGet, name, namespace, onError, opts); + + // Return getters and then the setters as the getters are more likely to be used with + // this function. + return [obj, error, setObj, setError]; + } + + _class() { + return this.constructor as KubeObjectClass; + } + + delete() { + const args: string[] = [this.getName()]; + if (this.isNamespaced) { + args.unshift(this.getNamespace()!); + } + + return this._class().apiEndpoint.delete(...args, {}, this._clusterName); + } + + update(data: KubeObjectInterface) { + return this._class().apiEndpoint.put(data, {}, this._clusterName); + } + + static put(data: KubeObjectInterface) { + return this.apiEndpoint.put(data); + } + + scale(numReplicas: number) { + const hasScaleApi = Object.keys(this._class().apiEndpoint).includes('scale'); + if (!hasScaleApi) { + throw new Error(`This class has no scale API: ${this._class().className}`); + } + + const spec = { + replicas: numReplicas, + }; + + type ApiEndpointWithScale = { + scale: { + patch: ( + body: { spec: { replicas: number } }, + metadata: KubeMetadata, + clusterName?: string + ) => Promise; + }; + }; + + return (this._class().apiEndpoint as ApiEndpointWithScale).scale.patch( + { + spec, + }, + this.metadata, + this._clusterName + ); + } + + patch(body: OpPatch[]) { + const patchMethod = this._class().apiEndpoint.patch; + const args: Parameters = [body]; + + if (this.isNamespaced) { + args.push(this.getNamespace()); + } + + args.push(this.getName()); + return this._class().apiEndpoint.patch(...args, {}, this._clusterName); + } + + /** Performs a request to check if the user has the given permission. + * @param reResourceAttrs The attributes describing this access request. See https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/self-subject-access-review-v1/#SelfSubjectAccessReviewSpec . + * @returns The result of the access request. + */ + static async fetchAuthorization(reqResourseAttrs?: AuthRequestResourceAttrs) { + // @todo: We should get the API info from the API endpoint. + const authApiVersions = ['v1', 'v1beta1']; + for (let j = 0; j < authApiVersions.length; j++) { + const authVersion = authApiVersions[j]; + + try { + return await post( + `/apis/authorization.k8s.io/${authVersion}/selfsubjectaccessreviews`, + { + kind: 'SelfSubjectAccessReview', + apiVersion: `authorization.k8s.io/${authVersion}`, + spec: { + resourceAttributes: reqResourseAttrs, + }, + }, + false + ); + } catch (err) { + // If this is the last attempt or the error is not 404, let it throw. + if ((err as ApiError).status !== 404 || j === authApiVersions.length - 1) { + throw err; + } + } + } + } + + static async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { + const resourceAttrs: AuthRequestResourceAttrs = { + verb, + ...reqResourseAttrs, + }; + + if (!resourceAttrs.resource) { + resourceAttrs['resource'] = this.pluralName; + } + + // @todo: We should get the API info from the API endpoint. + + // If we already have the group, version, and resource, then we can make the request + // without trying the API info, which may have several versions and thus be less optimal. + if (!!resourceAttrs.group && !!resourceAttrs.version && !!resourceAttrs.resource) { + return this.fetchAuthorization(resourceAttrs); + } + + // If we don't have the group, version, and resource, then we have to try all of the + // API info versions until we find one that works. + const apiInfo = this.apiEndpoint.apiInfo; + for (let i = 0; i < apiInfo.length; i++) { + const { group, version, resource } = apiInfo[i]; + // We only take from the details from the apiInfo if they're missing from the resourceAttrs. + // The idea is that, since this function may also be called from the instance's getAuthorization, + // it may already have the details from the instance's API version. + const attrs = { ...resourceAttrs }; + + if (!!attrs.resource) { + attrs.resource = resource; + } + if (!!attrs.group) { + attrs.group = group; + } + if (!!attrs.version) { + attrs.version = version; + } + + let authResult; + + try { + authResult = await this.fetchAuthorization(attrs); + } catch (err) { + // If this is the last attempt or the error is not 404, let it throw. + if ((err as ApiError).status !== 404 || i === apiInfo.length - 1) { + throw err; + } + } + + if (!!authResult) { + return authResult; + } + } + } + + async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { + const resourceAttrs: AuthRequestResourceAttrs = { + name: this.getName(), + verb, + ...reqResourseAttrs, + }; + + const namespace = this.getNamespace(); + if (!resourceAttrs.namespace && !!namespace) { + resourceAttrs['namespace'] = namespace; + } + + // Set up the group and version from the object's API version. + let [group, version] = this.jsonData?.apiVersion?.split('/') ?? []; + if (!version) { + version = group; + group = ''; + } + + if (!!group) { + resourceAttrs['group'] = group; + } + if (!!version) { + resourceAttrs['version'] = version; + } + + return this._class().getAuthorization(verb, resourceAttrs); + } + + static getErrorMessage(err: ApiError | null) { + if (!err) { + return null; + } + + switch (err.status) { + case 404: + return 'Error: Not found'; + case 403: + return 'Error: No permissions'; + default: + return 'Error'; + } + } +} diff --git a/frontend/src/lib/k8s/cluster.ts b/frontend/src/lib/k8s/cluster.ts index 5c776a92004..fb7e5607693 100644 --- a/frontend/src/lib/k8s/cluster.ts +++ b/frontend/src/lib/k8s/cluster.ts @@ -1,32 +1,17 @@ -import { OpPatch } from 'json-patch'; -import { JSONPath } from 'jsonpath-plus'; -import { cloneDeep, unset } from 'lodash'; -import React from 'react'; -import helpers from '../../helpers'; -import { createRouteURL } from '../router'; -import { getCluster, timeAgo, useErrorState } from '../util'; -import { useCluster, useConnectApi } from '.'; -import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy'; +import { QueryParameters } from './apiProxy'; import CronJob from './cronJob'; import DaemonSet from './daemonSet'; import Deployment from './deployment'; import { KubeEvent } from './event'; import Job from './job'; +import { KubeObject } from './KubeObject'; +import Pod from './pod'; import ReplicaSet from './replicaSet'; import StatefulSet from './statefulSet'; +export { KubeObject } from './KubeObject'; export const HEADLAMP_ALLOWED_NAMESPACES = 'headlamp.allowed-namespaces'; -function getAllowedNamespaces() { - const cluster = getCluster(); - if (!cluster) { - return []; - } - - const clusterSettings = helpers.loadClusterSettings(cluster); - return clusterSettings.allowedNamespaces || []; -} - export interface Cluster { name: string; useToken?: boolean; @@ -55,7 +40,13 @@ export interface KubeObjectInterface { kind: string; apiVersion?: string; metadata: KubeMetadata; - [otherProps: string]: any; + spec?: any; + status?: any; + items?: any[]; + actionType?: any; + lastTimestamp?: string; + key?: any; + // [otherProps: string]: any; } export interface StringDict { @@ -192,6 +183,7 @@ export interface KubeMetadata { * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids | UIDs docs} for more details. */ uid: string; + apiVersion?: any; } export interface KubeOwnerReference { @@ -288,38 +280,10 @@ export interface KubeManagedFieldsEntry { */ export interface KubeManagedFields extends KubeManagedFieldsEntry {} -// We have to define a KubeObject implementation here because the KubeObject -// class is defined within the function and therefore not inferable. -export interface KubeObjectIface { - apiList: ( - onList: (arg: InstanceType>[]) => void, - onError?: (err: ApiError) => void, - opts?: ApiListSingleNamespaceOptions - ) => any; - useApiList: ( - onList: (arg: InstanceType>[]) => void, - onError?: (err: ApiError) => void, - opts?: ApiListOptions - ) => any; - useApiGet: ( - onGet: (...args: any) => void, - name: string, - namespace?: string, - onError?: (err: ApiError) => void - ) => void; - useList: ( - opts?: ApiListOptions - ) => [any[], ApiError | null, (items: any[]) => void, (err: ApiError | null) => void]; - useGet: ( - name: string, - namespace?: string - ) => [any, ApiError | null, (item: any) => void, (err: ApiError | null) => void]; - getErrorMessage: (err?: ApiError | null) => string | null; - new (json: T): any; - className: string; - [prop: string]: any; - getAuthorization?: (arg: string, resourceAttrs?: AuthRequestResourceAttrs) => any; -} +/** + * @deprecated For backwards compatibility, please use KubeObject + */ +export type KubeObjectIface = any; export interface AuthRequestResourceAttrs { name?: string; @@ -330,554 +294,25 @@ export interface AuthRequestResourceAttrs { group?: string; verb?: string; } -type JsonPath = T extends object - ? { - [K in keyof T]: K extends string ? `${K}` | `${K}.${JsonPath}` : never; - }[keyof T] - : never; - -// @todo: uses of makeKubeObject somehow end up in an 'any' type. /** + * @deprecated For backwards compatibility, please extend KubeObject + * * @returns A KubeObject implementation for the given object name. * * @param objectName The name of the object to create a KubeObject implementation for. */ -export function makeKubeObject( - objectName: string -): KubeObjectIface { - class KubeObject { - static apiEndpoint: ReturnType; - jsonData: T | null = null; - public static readOnlyFields: JsonPath[]; - private readonly _clusterName: string; - - constructor(json: T) { - this.jsonData = json; - this._clusterName = getCluster() || ''; - } - - static get className(): string { - return objectName; - } - - get detailsRoute(): string { - return this._class().detailsRoute; - } - - static get detailsRoute(): string { - return this.className; - } - - static get pluralName(): string { - // This is a naive way to get the plural name of the object by default. It will - // work in most cases, but for exceptions (like Ingress), we must override this. - return this.className.toLowerCase() + 's'; - } - - get pluralName(): string { - // In case we need to override the plural name in instances. - return this._class().pluralName; - } - - get listRoute(): string { - return this._class().listRoute; - } - - static get listRoute(): string { - return this.detailsRoute + 's'; - } - - getDetailsLink() { - const params = { - namespace: this.getNamespace(), - name: this.getName(), - }; - const link = createRouteURL(this.detailsRoute, params); - return link; - } - - getListLink() { - return createRouteURL(this.listRoute); - } - - getName() { - return this.metadata.name; - } - - getNamespace() { - return this.metadata.namespace; - } - - getCreationTs() { - return this.metadata.creationTimestamp; - } - - getAge() { - return timeAgo(this.getCreationTs()); - } - - getValue(prop: string) { - return this.jsonData![prop]; - } - - get metadata() { - return this.jsonData!.metadata; - } - - get kind() { - return this.jsonData!.kind; - } - - get isNamespaced() { - return this._class().isNamespaced; - } - - static get isNamespaced() { - return this.apiEndpoint.isNamespaced; - } - - getEditableObject() { - const fieldsToRemove = this._class().readOnlyFields; - const code = this.jsonData ? cloneDeep(this.jsonData) : {}; - - fieldsToRemove?.forEach((path: JsonPath) => { - JSONPath({ - path, - json: code, - callback: (result, type, fullPayload) => { - if (fullPayload.parent && fullPayload.parentProperty) { - delete fullPayload.parent[fullPayload.parentProperty]; - } - }, - resultType: 'all', - }); - }); - - return code; - } - - // @todo: apiList has 'any' return type. - /** - * Returns the API endpoint for this object. - * - * @param onList - Callback function to be called when the list is retrieved. - * @param onError - Callback function to be called when an error occurs. - * @param opts - Options to be passed to the API endpoint. - * - * @returns The API endpoint for this object. - */ - static apiList( - onList: (arg: U[]) => void, - onError?: (err: ApiError) => void, - opts?: ApiListSingleNamespaceOptions - ) { - const createInstance = (item: T) => this.create(item) as U; - - const args: any[] = [(list: T[]) => onList(list.map((item: T) => createInstance(item) as U))]; - - if (this.apiEndpoint.isNamespaced) { - args.unshift(opts?.namespace || null); - } - - args.push(onError); - - const queryParams: QueryParameters = {}; - if (opts?.queryParams?.labelSelector) { - queryParams['labelSelector'] = opts.queryParams.labelSelector; - } - if (opts?.queryParams?.fieldSelector) { - queryParams['fieldSelector'] = opts.queryParams.fieldSelector; - } - if (opts?.queryParams?.limit) { - queryParams['limit'] = opts.queryParams.limit; - } - args.push(queryParams); - - args.push(opts?.cluster); - - return this.apiEndpoint.list.bind(null, ...args); - } - - static useApiList( - onList: (...arg: any[]) => any, - onError?: (err: ApiError) => void, - opts?: ApiListOptions - ) { - const [objs, setObjs] = React.useState<{ [key: string]: U[] }>({}); - const listCallback = onList as (arg: U[]) => void; - - function onObjs(namespace: string, objList: U[]) { - let newObjs: typeof objs = {}; - // Set the objects so we have them for the next API response... - setObjs(previousObjs => { - newObjs = { ...previousObjs, [namespace || '']: objList }; - return newObjs; - }); - - let allObjs: U[] = []; - Object.values(newObjs).map(currentObjs => { - allObjs = allObjs.concat(currentObjs); - }); - - listCallback(allObjs); - } - - const listCalls = []; - const queryParams = cloneDeep(opts); - let namespaces: string[] = []; - unset(queryParams, 'namespace'); - - const cluster = opts?.cluster; - - if (!!opts?.namespace) { - if (typeof opts.namespace === 'string') { - namespaces = [opts.namespace]; - } else if (Array.isArray(opts.namespace)) { - namespaces = opts.namespace as string[]; - } else { - throw Error('namespace should be a string or array of strings'); - } - } - - // If the request itself has no namespaces set, we check whether to apply the - // allowed namespaces. - if (namespaces.length === 0 && this.isNamespaced) { - namespaces = getAllowedNamespaces(); - } - - if (namespaces.length > 0) { - // If we have a namespace set, then we have to make an API call for each - // namespace and then set the objects once we have all of the responses. - for (const namespace of namespaces) { - listCalls.push( - this.apiList(objList => onObjs(namespace, objList as U[]), onError, { - namespace, - queryParams, - cluster, - }) - ); - } - } else { - // If we don't have a namespace set, then we only have one API call - // response to set and we return it right away. - listCalls.push(this.apiList(listCallback, onError, { queryParams, cluster })); - } - - useConnectApi(...listCalls); - } - - static useList( - opts?: ApiListOptions - ): [U[] | null, ApiError | null, (items: U[]) => void, (err: ApiError | null) => void] { - const [objList, setObjList] = React.useState(null); - const [error, setError] = useErrorState(setObjList); - const currentCluster = useCluster(); - const cluster = opts?.cluster || currentCluster; - - // Reset the list and error when the cluster changes. - React.useEffect(() => { - setObjList(null); - setError(null); - }, [cluster]); - - function setList(items: U[] | null) { - setObjList(items); - if (items !== null) { - setError(null); - } - } - - this.useApiList(setList, setError, opts); - - // Return getters and then the setters as the getters are more likely to be used with - // this function. - return [objList, error, setObjList, setError]; - } - - static create(this: new (arg: T) => U, item: T): U { - return new this(item) as U; - } - - static apiGet( - onGet: (...args: any) => void, - name: string, - namespace?: string, - onError?: (err: ApiError | null) => void, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) { - const createInstance = (item: T) => this.create(item) as U; - const args: any[] = [name, (obj: T) => onGet(createInstance(obj))]; - - if (this.apiEndpoint.isNamespaced) { - args.unshift(namespace); - } - - args.push(onError); - args.push(opts?.queryParams); - args.push(opts?.cluster); - - return this.apiEndpoint.get.bind(null, ...args); - } - - static useApiGet( - onGet: (...args: any) => any, - name: string, - namespace?: string, - onError?: (err: ApiError | null) => void, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) { - // We do the type conversion here because we want to be able to use hooks that may not have - // the exact signature as get callbacks. - const getCallback = onGet as (item: U) => void; - useConnectApi(this.apiGet(getCallback, name, namespace, onError, opts)); - } - - static useGet( - name: string, - namespace?: string, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ): [U | null, ApiError | null, (items: U) => void, (err: ApiError | null) => void] { - const [obj, setObj] = React.useState(null); - const [error, setError] = useErrorState(setObj); - - function onGet(item: U | null) { - // Only set the object if we have we have a different one. - if (!!obj && !!item && obj.metadata.resourceVersion === item.metadata.resourceVersion) { - return; - } - - setObj(item); - if (item !== null) { - setError(null); - } - } - - function onError(err: ApiError | null) { - if ( - error === err || - (!!error && !!err && error.message === err.message && error.status === err.status) - ) { - return; - } - - setError(err); - } - - this.useApiGet(onGet, name, namespace, onError, opts); - - // Return getters and then the setters as the getters are more likely to be used with - // this function. - return [obj, error, setObj, setError]; - } - - private _class() { - return this.constructor as typeof KubeObject; - } - - delete() { - const args: string[] = [this.getName()]; - if (this.isNamespaced) { - args.unshift(this.getNamespace()!); - } - - return this._class().apiEndpoint.delete(...args, {}, this._clusterName); - } - - update(data: KubeObjectInterface) { - return this._class().apiEndpoint.put(data, {}, this._clusterName); - } - - static put(data: KubeObjectInterface) { - return this.apiEndpoint.put(data); - } - - scale(numReplicas: number) { - const hasScaleApi = Object.keys(this._class().apiEndpoint).includes('scale'); - if (!hasScaleApi) { - throw new Error(`This class has no scale API: ${this._class().className}`); - } - - const spec = { - replicas: numReplicas, - }; - - type ApiEndpointWithScale = { - scale: { - patch: ( - body: { spec: { replicas: number } }, - metadata: KubeMetadata, - clusterName?: string - ) => Promise; - }; - }; - - return (this._class().apiEndpoint as ApiEndpointWithScale).scale.patch( - { - spec, - }, - this.metadata, - this._clusterName - ); - } - - patch(body: OpPatch[]) { - const patchMethod = this._class().apiEndpoint.patch; - const args: Parameters = [body]; - - if (this.isNamespaced) { - args.push(this.getNamespace()); - } - - args.push(this.getName()); - return this._class().apiEndpoint.patch(...args, {}, this._clusterName); - } - - /** Performs a request to check if the user has the given permission. - * @param reResourceAttrs The attributes describing this access request. See https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/self-subject-access-review-v1/#SelfSubjectAccessReviewSpec . - * @returns The result of the access request. - */ - private static async fetchAuthorization(reqResourseAttrs?: AuthRequestResourceAttrs) { - // @todo: We should get the API info from the API endpoint. - const authApiVersions = ['v1', 'v1beta1']; - for (let j = 0; j < authApiVersions.length; j++) { - const authVersion = authApiVersions[j]; - - try { - return await post( - `/apis/authorization.k8s.io/${authVersion}/selfsubjectaccessreviews`, - { - kind: 'SelfSubjectAccessReview', - apiVersion: `authorization.k8s.io/${authVersion}`, - spec: { - resourceAttributes: reqResourseAttrs, - }, - }, - false - ); - } catch (err) { - // If this is the last attempt or the error is not 404, let it throw. - if ((err as ApiError).status !== 404 || j === authApiVersions.length - 1) { - throw err; - } - } - } - } - - static async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { - const resourceAttrs: AuthRequestResourceAttrs = { - verb, - ...reqResourseAttrs, - }; - - if (!resourceAttrs.resource) { - resourceAttrs['resource'] = this.pluralName; - } - - // @todo: We should get the API info from the API endpoint. - - // If we already have the group, version, and resource, then we can make the request - // without trying the API info, which may have several versions and thus be less optimal. - if (!!resourceAttrs.group && !!resourceAttrs.version && !!resourceAttrs.resource) { - return this.fetchAuthorization(resourceAttrs); - } - - // If we don't have the group, version, and resource, then we have to try all of the - // API info versions until we find one that works. - const apiInfo = this.apiEndpoint.apiInfo; - for (let i = 0; i < apiInfo.length; i++) { - const { group, version, resource } = apiInfo[i]; - // We only take from the details from the apiInfo if they're missing from the resourceAttrs. - // The idea is that, since this function may also be called from the instance's getAuthorization, - // it may already have the details from the instance's API version. - const attrs = { ...resourceAttrs }; - - if (!!attrs.resource) { - attrs.resource = resource; - } - if (!!attrs.group) { - attrs.group = group; - } - if (!!attrs.version) { - attrs.version = version; - } - - let authResult; - - try { - authResult = await this.fetchAuthorization(attrs); - } catch (err) { - // If this is the last attempt or the error is not 404, let it throw. - if ((err as ApiError).status !== 404 || i === apiInfo.length - 1) { - throw err; - } - } - - if (!!authResult) { - return authResult; - } - } - } - - async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { - const resourceAttrs: AuthRequestResourceAttrs = { - name: this.getName(), - verb, - ...reqResourseAttrs, - }; - - const namespace = this.getNamespace(); - if (!resourceAttrs.namespace && !!namespace) { - resourceAttrs['namespace'] = namespace; - } - - // Set up the group and version from the object's API version. - let [group, version] = this.jsonData?.apiVersion?.split('/') ?? []; - if (!version) { - version = group; - group = ''; - } - - if (!!group) { - resourceAttrs['group'] = group; - } - if (!!version) { - resourceAttrs['version'] = version; - } - - return this._class().getAuthorization(verb, resourceAttrs); - } - - static getErrorMessage(err: ApiError | null) { - if (!err) { - return null; - } - - switch (err.status) { - case 404: - return 'Error: Not found'; - case 403: - return 'Error: No permissions'; - default: - return 'Error'; - } - } +export function makeKubeObject(objectName: string) { + class KubeObjectInternal extends KubeObject { + static objectName = objectName; } - - return KubeObject as KubeObjectIface; + return KubeObjectInternal; } -export type KubeObjectClass = ReturnType; -export type KubeObject = InstanceType; +/** + * This type refers to the *class* of a KubeObject. + */ +export type KubeObjectClass = typeof KubeObject; export type Time = number | string | null; @@ -1282,4 +717,13 @@ export interface KubeContainerStatus { started?: boolean; } -export type Workload = DaemonSet | ReplicaSet | StatefulSet | Job | CronJob | Deployment; +export type Workload = Pod | DaemonSet | ReplicaSet | StatefulSet | Job | CronJob | Deployment; + +export type WorkloadClass = + | typeof Pod + | typeof DaemonSet + | typeof ReplicaSet + | typeof StatefulSet + | typeof Job + | typeof CronJob + | typeof Deployment; diff --git a/frontend/src/lib/k8s/configMap.ts b/frontend/src/lib/k8s/configMap.ts index 9ac7f7906dd..e5e77e75284 100644 --- a/frontend/src/lib/k8s/configMap.ts +++ b/frontend/src/lib/k8s/configMap.ts @@ -1,15 +1,16 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject, StringDict } from './cluster'; +import { KubeObject, KubeObjectInterface, StringDict } from './cluster'; export interface KubeConfigMap extends KubeObjectInterface { data: StringDict; } -class ConfigMap extends makeKubeObject('configMap') { +class ConfigMap extends KubeObject { + static objectName = 'configMap'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'configmaps'); get data() { - return this.jsonData?.data; + return this.jsonData.data; } } diff --git a/frontend/src/lib/k8s/crd.ts b/frontend/src/lib/k8s/crd.ts index 91f7763e4ee..bda6737f9dc 100644 --- a/frontend/src/lib/k8s/crd.ts +++ b/frontend/src/lib/k8s/crd.ts @@ -1,6 +1,6 @@ import { ResourceClasses } from '.'; import { apiFactory, apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectClass, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectClass, KubeObjectInterface } from './cluster'; export interface KubeCRD extends KubeObjectInterface { spec: { @@ -47,7 +47,8 @@ export interface KubeCRD extends KubeObjectInterface { }; } -class CustomResourceDefinition extends makeKubeObject('crd') { +class CustomResourceDefinition extends KubeObject { + static objectName = 'crd'; static apiEndpoint = apiFactory( ['apiextensions.k8s.io', 'v1', 'customresourcedefinitions'], ['apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions'] @@ -63,11 +64,11 @@ class CustomResourceDefinition extends makeKubeObject('crd') { } get spec(): KubeCRD['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubeCRD['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get plural(): string { @@ -98,7 +99,7 @@ class CustomResourceDefinition extends makeKubeObject('crd') { return this.spec.scope === 'Namespaced'; } - makeCRClass(): KubeObjectClass { + makeCRClass(): typeof KubeObject { const apiInfo: CRClassArgs['apiInfo'] = (this.jsonData as KubeCRD).spec.versions.map( versionInfo => ({ group: this.spec.group, version: versionInfo.name }) ); @@ -130,12 +131,12 @@ export interface CRClassArgs { export function makeCustomResourceClass( args: [group: string, version: string, pluralName: string][], isNamespaced: boolean -): ReturnType; -export function makeCustomResourceClass(args: CRClassArgs): ReturnType; +): KubeObjectClass; +export function makeCustomResourceClass(args: CRClassArgs): KubeObjectClass; export function makeCustomResourceClass( args: [group: string, version: string, pluralName: string][] | CRClassArgs, isNamespaced?: boolean -): ReturnType { +): KubeObjectClass { let apiInfoArgs: [group: string, version: string, pluralName: string][] = []; if (Array.isArray(args)) { @@ -146,7 +147,7 @@ export function makeCustomResourceClass( // Used for tests if (import.meta.env.UNDER_TEST === 'true') { - const knownClass = ResourceClasses[apiInfoArgs[0][2]]; + const knownClass = (ResourceClasses as Record)[apiInfoArgs[0][2]]; if (!!knownClass) { return knownClass; } @@ -159,7 +160,8 @@ export function makeCustomResourceClass( }; const apiFunc = !!objArgs.isNamespaced ? apiFactoryWithNamespace : apiFactory; - return class CRClass extends makeKubeObject(objArgs.singleName) { + return class CRClass extends KubeObject { + static objectName = objArgs.singleName; static apiEndpoint = apiFunc(...apiInfoArgs); }; } diff --git a/frontend/src/lib/k8s/cronJob.ts b/frontend/src/lib/k8s/cronJob.ts index 25e2a189ebf..d1c397c6c79 100644 --- a/frontend/src/lib/k8s/cronJob.ts +++ b/frontend/src/lib/k8s/cronJob.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeContainer, KubeMetadata, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeContainer, KubeMetadata, KubeObject, KubeObjectInterface } from './cluster'; /** * CronJob structure returned by the k8s API. @@ -34,7 +34,8 @@ export interface KubeCronJob extends KubeObjectInterface { }; } -class CronJob extends makeKubeObject('CronJob') { +class CronJob extends KubeObject { + static objectName = 'CronJob'; static apiEndpoint = apiFactoryWithNamespace( ['batch', 'v1', 'cronjobs'], ['batch', 'v1beta1', 'cronjobs'] diff --git a/frontend/src/lib/k8s/daemonSet.ts b/frontend/src/lib/k8s/daemonSet.ts index 82cfbcf6a85..c264a1e9718 100644 --- a/frontend/src/lib/k8s/daemonSet.ts +++ b/frontend/src/lib/k8s/daemonSet.ts @@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy'; import { KubeContainer, KubeMetadata, + KubeObject, KubeObjectInterface, LabelSelector, - makeKubeObject, } from './cluster'; import { KubePodSpec } from './pod'; @@ -28,15 +28,16 @@ export interface KubeDaemonSet extends KubeObjectInterface { }; } -class DaemonSet extends makeKubeObject('DaemonSet') { +class DaemonSet extends KubeObject { + static objectName = 'DaemonSet'; static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'daemonsets'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/deployment.ts b/frontend/src/lib/k8s/deployment.ts index b02bb34e4ec..0350f54462b 100644 --- a/frontend/src/lib/k8s/deployment.ts +++ b/frontend/src/lib/k8s/deployment.ts @@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy'; import { KubeContainer, KubeMetadata, + KubeObject, KubeObjectInterface, LabelSelector, - makeKubeObject, } from './cluster'; import { KubePodSpec } from './pod'; @@ -26,7 +26,8 @@ export interface KubeDeployment extends KubeObjectInterface { }; } -class Deployment extends makeKubeObject('Deployment') { +class Deployment extends KubeObject { + static objectName = 'Deployment'; static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'deployments', true); get spec() { diff --git a/frontend/src/lib/k8s/endpoints.ts b/frontend/src/lib/k8s/endpoints.ts index 90d4e731e64..dc73d101307 100644 --- a/frontend/src/lib/k8s/endpoints.ts +++ b/frontend/src/lib/k8s/endpoints.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeMetadata, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeMetadata, KubeObject, KubeObjectInterface } from './cluster'; export interface KubeEndpointPort { name?: string; @@ -28,19 +28,20 @@ export interface KubeEndpoint extends KubeObjectInterface { subsets: KubeEndpointSubset[]; } -class Endpoints extends makeKubeObject('endpoint') { +class Endpoints extends KubeObject { + static objectName = 'endpoint'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'endpoints'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } get subsets() { - return this.jsonData!.subsets; + return this.jsonData.subsets; } getAddressesText() { diff --git a/frontend/src/lib/k8s/event.ts b/frontend/src/lib/k8s/event.ts index 5ede3b043ad..02b8487be42 100644 --- a/frontend/src/lib/k8s/event.ts +++ b/frontend/src/lib/k8s/event.ts @@ -2,7 +2,7 @@ import React from 'react'; import { CancellablePromise, ResourceClasses } from '.'; import { ApiError, apiFactoryWithNamespace, QueryParameters } from './apiProxy'; import { request } from './apiProxy'; -import { KubeMetadata, KubeObject, makeKubeObject } from './cluster'; +import { KubeMetadata, KubeObject, KubeObjectClass } from './cluster'; export interface KubeEvent { type: string; @@ -21,7 +21,8 @@ export interface KubeEvent { [otherProps: string]: any; } -class Event extends makeKubeObject('Event') { +class Event extends KubeObject { + static objectName = 'Event'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'events'); // Max number of events to fetch from the API @@ -105,7 +106,7 @@ class Event extends makeKubeObject('Event') { return eventTime; } - const firstTimestamp = this.firstTimestamp; + const firstTimestamp = this.getValue('firstTimestamp'); if (!!firstTimestamp) { return firstTimestamp; } @@ -149,7 +150,9 @@ class Event extends makeKubeObject('Event') { return null; } - const InvolvedObjectClass = ResourceClasses[this.involvedObject.kind]; + const InvolvedObjectClass = (ResourceClasses as Record)[ + this.involvedObject.kind + ]; let objInstance: KubeObject | null = null; if (!!InvolvedObjectClass) { objInstance = new InvolvedObjectClass({ @@ -157,7 +160,7 @@ class Event extends makeKubeObject('Event') { metadata: { name: this.involvedObject.name, namespace: this.involvedObject.namespace, - }, + } as KubeMetadata, }); } diff --git a/frontend/src/lib/k8s/hpa.ts b/frontend/src/lib/k8s/hpa.ts index b68ffe0695a..9e1cfb7756f 100644 --- a/frontend/src/lib/k8s/hpa.ts +++ b/frontend/src/lib/k8s/hpa.ts @@ -1,6 +1,6 @@ import { ResourceClasses } from '.'; import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObject, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeMetadata, KubeObject, KubeObjectClass, KubeObjectInterface } from './cluster'; export interface CrossVersionObjectReference { apiVersion: string; kind: string; @@ -166,15 +166,16 @@ interface HPAMetrics { shortValue: string; } -class HPA extends makeKubeObject('horizontalPodAutoscaler') { +class HPA extends KubeObject { + static objectName = 'horizontalPodAutoscaler'; static apiEndpoint = apiFactoryWithNamespace('autoscaling', 'v2', 'horizontalpodautoscalers'); get spec(): HpaSpec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): HpaStatus { - return this.jsonData!.status; + return this.jsonData.status; } metrics(t: Function): HPAMetrics[] { @@ -334,12 +335,12 @@ class HPA extends makeKubeObject('horizontalPodAutoscaler') { } get referenceObject(): KubeObject | null { - const target = this.jsonData?.spec?.scaleTargetRef; + const target = this.jsonData.spec?.scaleTargetRef; if (!target) { return null; } - const TargetObjectClass = ResourceClasses[target.kind]; + const TargetObjectClass = (ResourceClasses as Record)[target.kind]; let objInstance: KubeObject | null = null; if (!!TargetObjectClass) { objInstance = new TargetObjectClass({ @@ -347,7 +348,7 @@ class HPA extends makeKubeObject('horizontalPodAutoscaler') { metadata: { name: target.name, namespace: this.getNamespace(), - }, + } as KubeMetadata, }); } diff --git a/frontend/src/lib/k8s/index.test.ts b/frontend/src/lib/k8s/index.test.ts index 9022e5ec88a..6f1505c7165 100644 --- a/frontend/src/lib/k8s/index.test.ts +++ b/frontend/src/lib/k8s/index.test.ts @@ -244,7 +244,7 @@ const namespacedClasses = [ ]; describe('Test class namespaces', () => { - const classCopy = { ...ResourceClasses }; + const classCopy: Record = { ...ResourceClasses }; namespacedClasses.forEach(cls => { test(`Check namespaced ${cls}`, () => { expect(classCopy[cls]).toBeDefined(); diff --git a/frontend/src/lib/k8s/index.ts b/frontend/src/lib/k8s/index.ts index 3991862a185..f0175ba80e0 100644 --- a/frontend/src/lib/k8s/index.ts +++ b/frontend/src/lib/k8s/index.ts @@ -5,7 +5,7 @@ import { ConfigState } from '../../redux/configSlice'; import { useTypedSelector } from '../../redux/reducers/reducers'; import { getCluster, getClusterPrefixedPath } from '../util'; import { ApiError, clusterRequest } from './apiProxy'; -import { Cluster, KubeObject, LabelSelector, StringDict } from './cluster'; +import { Cluster, LabelSelector, StringDict } from './cluster'; import ClusterRole from './clusterRole'; import ClusterRoleBinding from './clusterRoleBinding'; import ConfigMap from './configMap'; @@ -39,53 +39,40 @@ import ServiceAccount from './serviceAccount'; import StatefulSet from './statefulSet'; import StorageClass from './storageClass'; -const classList = [ - ClusterRole, - ClusterRoleBinding, - ConfigMap, - CustomResourceDefinition, - CronJob, - DaemonSet, - Deployment, - Endpoints, - LimitRange, - Lease, - ResourceQuota, - HPA, - PodDisruptionBudget, - PriorityClass, - Ingress, - IngressClass, - Job, - Namespace, - NetworkPolicy, - Node, - PersistentVolume, - PersistentVolumeClaim, - Pod, - ReplicaSet, - Role, - RoleBinding, - RuntimeClass, - Secret, - Service, - ServiceAccount, - StatefulSet, - StorageClass, -]; - -const resourceClassesDict: { - [className: string]: KubeObject; -} = {}; - -classList.forEach(cls => { - // Ideally this should just be the class name, but until we ensure the class name is consistent - // (in what comes to the capitalization), we use this lazy approach. - const className: string = cls.className.charAt(0).toUpperCase() + cls.className.slice(1); - resourceClassesDict[className] = cls; -}); - -export const ResourceClasses = resourceClassesDict; +export const ResourceClasses = { + ClusterRole: ClusterRole, + ClusterRoleBinding: ClusterRoleBinding, + ConfigMap: ConfigMap, + CustomResourceDefinition: CustomResourceDefinition, + CronJob: CronJob, + DaemonSet: DaemonSet, + Deployment: Deployment, + Endpoint: Endpoints, + LimitRange: LimitRange, + Lease: Lease, + ResourceQuota: ResourceQuota, + HorizontalPodAutoscaler: HPA, + PodDisruptionBudget: PodDisruptionBudget, + PriorityClass: PriorityClass, + Ingress: Ingress, + IngressClass: IngressClass, + Job: Job, + Namespace: Namespace, + NetworkPolicy: NetworkPolicy, + Node: Node, + PersistentVolume: PersistentVolume, + PersistentVolumeClaim: PersistentVolumeClaim, + Pod: Pod, + ReplicaSet: ReplicaSet, + Role: Role, + RoleBinding: RoleBinding, + RuntimeClass: RuntimeClass, + Secret: Secret, + Service: Service, + ServiceAccount: ServiceAccount, + StatefulSet: StatefulSet, + StorageClass: StorageClass, +}; /** Hook for getting or fetching the clusters configuration. * This gets the clusters from the redux store. The redux store is updated diff --git a/frontend/src/lib/k8s/ingress.ts b/frontend/src/lib/k8s/ingress.ts index b546e581f21..6f4610d2bd6 100644 --- a/frontend/src/lib/k8s/ingress.ts +++ b/frontend/src/lib/k8s/ingress.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; interface LegacyIngressRule { host: string; @@ -68,7 +68,8 @@ export interface KubeIngress extends KubeObjectInterface { }; } -class Ingress extends makeKubeObject('ingress') { +class Ingress extends KubeObject { + static objectName = 'ingress'; static apiEndpoint = apiFactoryWithNamespace( ['networking.k8s.io', 'v1', 'ingresses'], ['extensions', 'v1beta1', 'ingresses'] @@ -77,7 +78,7 @@ class Ingress extends makeKubeObject('ingress') { private cachedRules: IngressRule[] = []; get spec(): KubeIngress['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } getHosts() { diff --git a/frontend/src/lib/k8s/ingressClass.ts b/frontend/src/lib/k8s/ingressClass.ts index 79db73b4f77..43ead5fa2e2 100644 --- a/frontend/src/lib/k8s/ingressClass.ts +++ b/frontend/src/lib/k8s/ingressClass.ts @@ -1,5 +1,5 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeIngressClass extends KubeObjectInterface { spec: { @@ -8,15 +8,16 @@ export interface KubeIngressClass extends KubeObjectInterface { }; } -class IngressClass extends makeKubeObject('ingressClass') { +class IngressClass extends KubeObject { + static objectName = 'ingressClass'; static apiEndpoint = apiFactory(['networking.k8s.io', 'v1', 'ingressclasses']); get spec(): KubeIngressClass['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get isDefault(): boolean { - const annotations = this.jsonData!.metadata?.annotations; + const annotations = this.jsonData.metadata?.annotations; if (annotations !== undefined) { return annotations['ingressclass.kubernetes.io/is-default-class'] === 'true'; } diff --git a/frontend/src/lib/k8s/job.ts b/frontend/src/lib/k8s/job.ts index c2ec2963d15..991b741ba1a 100644 --- a/frontend/src/lib/k8s/job.ts +++ b/frontend/src/lib/k8s/job.ts @@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy'; import { KubeContainer, KubeMetadata, + KubeObject, KubeObjectInterface, LabelSelector, - makeKubeObject, } from './cluster'; import { KubePodSpec } from './pod'; @@ -22,15 +22,16 @@ export interface KubeJob extends KubeObjectInterface { }; } -class Job extends makeKubeObject('Job') { +class Job extends KubeObject { + static objectName = 'Job'; static apiEndpoint = apiFactoryWithNamespace('batch', 'v1', 'jobs'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/lease.ts b/frontend/src/lib/k8s/lease.ts index 8df4801b404..c26e83e9807 100644 --- a/frontend/src/lib/k8s/lease.ts +++ b/frontend/src/lib/k8s/lease.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface LeaseSpec { holderIdentity: string; @@ -12,10 +12,11 @@ export interface KubeLease extends KubeObjectInterface { spec: LeaseSpec; } -export class Lease extends makeKubeObject('Lease') { +export class Lease extends KubeObject { + static objectName = 'Lease'; static apiEndpoint = apiFactoryWithNamespace('coordination.k8s.io', 'v1', 'leases'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } } diff --git a/frontend/src/lib/k8s/limitRange.tsx b/frontend/src/lib/k8s/limitRange.tsx index d2007a14081..d9f0976ea41 100644 --- a/frontend/src/lib/k8s/limitRange.tsx +++ b/frontend/src/lib/k8s/limitRange.tsx @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface LimitRangeSpec { limits: { @@ -27,10 +27,11 @@ export interface KubeLimitRange extends KubeObjectInterface { spec: LimitRangeSpec; } -export class LimitRange extends makeKubeObject('LimitRange') { +export class LimitRange extends KubeObject { + static objectName = 'LimitRange'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'limitranges'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } } diff --git a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts index c51aba26927..6a26892f57e 100644 --- a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts @@ -1,5 +1,5 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface, LabelSelector } from './cluster'; export interface KubeRuleWithOperations { apiGroups: string[]; @@ -42,9 +42,8 @@ export interface KubeMutatingWebhookConfiguration extends KubeObjectInterface { }[]; } -class MutatingWebhookConfiguration extends makeKubeObject( - 'MutatingWebhookConfiguration' -) { +class MutatingWebhookConfiguration extends KubeObject { + static objectName = 'MutatingWebhookConfiguration'; static apiEndpoint = apiFactory( 'admissionregistration.k8s.io', 'v1', @@ -52,7 +51,7 @@ class MutatingWebhookConfiguration extends makeKubeObject('namespace') { +class Namespace extends KubeObject { + static objectName = 'namespace'; static apiEndpoint = apiFactory('', 'v1', 'namespaces'); get status() { - return this.jsonData!.status; + return this.jsonData.status; } /** diff --git a/frontend/src/lib/k8s/networkpolicy.tsx b/frontend/src/lib/k8s/networkpolicy.tsx index 0096e80cd67..6d2333ff145 100644 --- a/frontend/src/lib/k8s/networkpolicy.tsx +++ b/frontend/src/lib/k8s/networkpolicy.tsx @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface, LabelSelector } from './cluster'; export interface NetworkPolicyPort { port?: string | number; @@ -35,7 +35,8 @@ export interface KubeNetworkPolicy extends KubeObjectInterface { policyTypes: string[]; } -class NetworkPolicy extends makeKubeObject('NetworkPolicy') { +class NetworkPolicy extends KubeObject { + static objectName = 'NetworkPolicy'; static apiEndpoint = apiFactoryWithNamespace('networking.k8s.io', 'v1', 'networkpolicies'); static get pluralName() { diff --git a/frontend/src/lib/k8s/node.ts b/frontend/src/lib/k8s/node.ts index 45d8e4dc550..acf87b5963a 100644 --- a/frontend/src/lib/k8s/node.ts +++ b/frontend/src/lib/k8s/node.ts @@ -2,7 +2,7 @@ import React from 'react'; import { useErrorState } from '../util'; import { useConnectApi } from '.'; import { ApiError, apiFactory, metrics } from './apiProxy'; -import { KubeCondition, KubeMetrics, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeCondition, KubeMetrics, KubeObject, KubeObjectInterface } from './cluster'; export interface KubeNode extends KubeObjectInterface { status: { @@ -52,15 +52,16 @@ export interface KubeNode extends KubeObjectInterface { }; } -class Node extends makeKubeObject('node') { +class Node extends KubeObject { + static objectName = 'node'; static apiEndpoint = apiFactory('', 'v1', 'nodes'); get status(): KubeNode['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get spec(): KubeNode['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } static useMetrics(): [KubeMetrics[] | null, ApiError | null] { diff --git a/frontend/src/lib/k8s/persistentVolume.ts b/frontend/src/lib/k8s/persistentVolume.ts index bfaeb0ed9db..f394e9e3ddc 100644 --- a/frontend/src/lib/k8s/persistentVolume.ts +++ b/frontend/src/lib/k8s/persistentVolume.ts @@ -1,5 +1,5 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubePersistentVolume extends KubeObjectInterface { spec: { @@ -15,15 +15,16 @@ export interface KubePersistentVolume extends KubeObjectInterface { }; } -class PersistentVolume extends makeKubeObject('persistentVolume') { +class PersistentVolume extends KubeObject { + static objectName = 'persistentVolume'; static apiEndpoint = apiFactory('', 'v1', 'persistentvolumes'); get spec() { - return this.jsonData?.spec; + return this.jsonData.spec; } get status() { - return this.jsonData?.status; + return this.jsonData.status; } } diff --git a/frontend/src/lib/k8s/persistentVolumeClaim.ts b/frontend/src/lib/k8s/persistentVolumeClaim.ts index 9c4ec82a462..4af847a9abb 100644 --- a/frontend/src/lib/k8s/persistentVolumeClaim.ts +++ b/frontend/src/lib/k8s/persistentVolumeClaim.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubePersistentVolumeClaim extends KubeObjectInterface { spec?: { @@ -26,17 +26,16 @@ export interface KubePersistentVolumeClaim extends KubeObjectInterface { }; } -class PersistentVolumeClaim extends makeKubeObject( - 'persistentVolumeClaim' -) { +class PersistentVolumeClaim extends KubeObject { + static objectName = 'persistentVolumeClaim'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'persistentvolumeclaims'); get spec() { - return this.jsonData?.spec; + return this.jsonData.spec; } get status() { - return this.jsonData?.status; + return this.jsonData.status; } } diff --git a/frontend/src/lib/k8s/pod.ts b/frontend/src/lib/k8s/pod.ts index dadb7c00a2f..5dd9fd09650 100644 --- a/frontend/src/lib/k8s/pod.ts +++ b/frontend/src/lib/k8s/pod.ts @@ -4,8 +4,8 @@ import { KubeCondition, KubeContainer, KubeContainerStatus, + KubeObject, KubeObjectInterface, - makeKubeObject, Time, } from './cluster'; @@ -26,6 +26,10 @@ export interface KubePodSpec { conditionType: string; }[]; volumes?: KubeVolume[]; + serviceAccountName?: string; + serviceAccount?: string; + priority?: string; + tolerations?: any[]; } export interface KubePod extends KubeObjectInterface { @@ -86,7 +90,8 @@ type PodDetailedStatus = { lastRestartDate: Date; }; -class Pod extends makeKubeObject('Pod') { +class Pod extends KubeObject { + static objectName = 'Pod'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'pods'); protected detailedStatusCache: Partial<{ resourceVersion: string; details: PodDetailedStatus }>; @@ -96,11 +101,11 @@ class Pod extends makeKubeObject('Pod') { } get spec(): KubePod['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubePod['status'] { - return this.jsonData!.status; + return this.jsonData.status; } getLogs(...args: Parameters): () => void { diff --git a/frontend/src/lib/k8s/podDisruptionBudget.ts b/frontend/src/lib/k8s/podDisruptionBudget.ts index ad2611f24d9..c0ca06285dc 100644 --- a/frontend/src/lib/k8s/podDisruptionBudget.ts +++ b/frontend/src/lib/k8s/podDisruptionBudget.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubePDB extends KubeObjectInterface { spec: { @@ -36,15 +36,16 @@ export interface KubePDB extends KubeObjectInterface { }; } -class PDB extends makeKubeObject('podDisruptionBudget') { +class PDB extends KubeObject { + static objectName = 'podDisruptionBudget'; static apiEndpoint = apiFactoryWithNamespace(['policy', 'v1', 'poddisruptionbudgets']); get spec(): KubePDB['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubePDB['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get selectors(): string[] { diff --git a/frontend/src/lib/k8s/priorityClass.ts b/frontend/src/lib/k8s/priorityClass.ts index d4c0f7c9ede..bcb83c1158f 100644 --- a/frontend/src/lib/k8s/priorityClass.ts +++ b/frontend/src/lib/k8s/priorityClass.ts @@ -1,14 +1,15 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubePriorityClass extends KubeObjectInterface { value: number; preemptionPolicy: string; - globalDefault?: boolean; + globalDefault?: boolean | null; description: string; } -class PriorityClass extends makeKubeObject('priorityClass') { +class PriorityClass extends KubeObject { + static objectName = 'priorityClass'; static apiEndpoint = apiFactory('scheduling.k8s.io', 'v1', 'priorityclasses'); static get pluralName(): string { @@ -19,20 +20,20 @@ class PriorityClass extends makeKubeObject('priorityClass') { return 'priorityclasses'; } - get value(): string { - return this.jsonData!.value; + get value(): number { + return this.jsonData.value; } get globalDefault(): boolean | null { - return this.jsonData.globalDefault; + return this.jsonData.globalDefault!; } get description(): string { - return this.jsonData!.description; + return this.jsonData.description; } get preemptionPolicy(): string { - return this.jsonData!.preemptionPolicy; + return this.jsonData.preemptionPolicy; } } diff --git a/frontend/src/lib/k8s/replicaSet.ts b/frontend/src/lib/k8s/replicaSet.ts index 2e4b80adab5..dcef6cbcaaa 100644 --- a/frontend/src/lib/k8s/replicaSet.ts +++ b/frontend/src/lib/k8s/replicaSet.ts @@ -3,9 +3,9 @@ import { KubeCondition, KubeContainer, KubeMetadata, + KubeObject, KubeObjectInterface, LabelSelector, - makeKubeObject, } from './cluster'; import { KubePodSpec } from './pod'; @@ -30,15 +30,16 @@ export interface KubeReplicaSet extends KubeObjectInterface { }; } -class ReplicaSet extends makeKubeObject('ReplicaSet') { +class ReplicaSet extends KubeObject { + static objectName = 'ReplicaSet'; static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'replicasets', true); get spec(): KubeReplicaSet['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubeReplicaSet['status'] { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/resourceQuota.ts b/frontend/src/lib/k8s/resourceQuota.ts index 2bd2b22bcfd..806767048fa 100644 --- a/frontend/src/lib/k8s/resourceQuota.ts +++ b/frontend/src/lib/k8s/resourceQuota.ts @@ -1,6 +1,6 @@ import { normalizeUnit } from '../util'; import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; interface spec { hard: { @@ -30,15 +30,16 @@ export interface KubeResourceQuota extends KubeObjectInterface { status: status; } -class ResourceQuota extends makeKubeObject('resourceQuota') { +class ResourceQuota extends KubeObject { + static objectName = 'resourceQuota'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'resourcequotas'); get spec(): spec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): status { - return this.jsonData!.status; + return this.jsonData.status; } get requests(): string[] { diff --git a/frontend/src/lib/k8s/role.ts b/frontend/src/lib/k8s/role.ts index 50deb3d3faf..5984eb41bce 100644 --- a/frontend/src/lib/k8s/role.ts +++ b/frontend/src/lib/k8s/role.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeRole extends KubeObjectInterface { rules: { @@ -8,14 +8,15 @@ export interface KubeRole extends KubeObjectInterface { resourceNames: string[]; resources: string[]; verbs: string[]; - }; + }[]; } -class Role extends makeKubeObject('role') { +class Role extends KubeObject { + static objectName = 'role'; static apiEndpoint = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'roles'); get rules() { - return this.jsonData!.rules; + return this.jsonData.rules; } } diff --git a/frontend/src/lib/k8s/roleBinding.ts b/frontend/src/lib/k8s/roleBinding.ts index 87f927fd4a1..f0599eaa223 100644 --- a/frontend/src/lib/k8s/roleBinding.ts +++ b/frontend/src/lib/k8s/roleBinding.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeRoleBinding extends KubeObjectInterface { roleRef: { @@ -15,15 +15,16 @@ export interface KubeRoleBinding extends KubeObjectInterface { }[]; } -class RoleBinding extends makeKubeObject('roleBinding') { +class RoleBinding extends KubeObject { + static objectName = 'roleBinding'; static apiEndpoint = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'rolebindings'); get roleRef() { - return this.jsonData!.roleRef; + return this.jsonData.roleRef; } get subjects(): KubeRoleBinding['subjects'] { - return this.jsonData!.subjects; + return this.jsonData.subjects; } } diff --git a/frontend/src/lib/k8s/runtime.ts b/frontend/src/lib/k8s/runtime.ts index 3c821fe7704..c0bd21f4735 100644 --- a/frontend/src/lib/k8s/runtime.ts +++ b/frontend/src/lib/k8s/runtime.ts @@ -1,15 +1,18 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeRuntimeClass extends KubeObjectInterface { handler: string; + overhead?: any; + scheduling?: any; } -export class RuntimeClass extends makeKubeObject('RuntimeClass') { +export class RuntimeClass extends KubeObject { + static objectName = 'RuntimeClass'; static apiEndpoint = apiFactory('node.k8s.io', 'v1', 'runtimeclasses'); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } static get pluralName() { diff --git a/frontend/src/lib/k8s/secret.ts b/frontend/src/lib/k8s/secret.ts index 6cac08ea229..6f87cea648d 100644 --- a/frontend/src/lib/k8s/secret.ts +++ b/frontend/src/lib/k8s/secret.ts @@ -1,20 +1,21 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject, StringDict } from './cluster'; +import { KubeObject, KubeObjectInterface, StringDict } from './cluster'; export interface KubeSecret extends KubeObjectInterface { data: StringDict; type: string; } -class Secret extends makeKubeObject('secret') { +class Secret extends KubeObject { + static objectName = 'Secret'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'secrets'); get data() { - return this.jsonData!.data; + return this.jsonData.data; } get type() { - return this.jsonData!.type; + return this.jsonData.type; } } diff --git a/frontend/src/lib/k8s/service.ts b/frontend/src/lib/k8s/service.ts index 89d474cfa08..80469e4e075 100644 --- a/frontend/src/lib/k8s/service.ts +++ b/frontend/src/lib/k8s/service.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeCondition, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeCondition, KubeObject, KubeObjectInterface } from './cluster'; export interface KubePortStatus { error?: string; @@ -39,15 +39,16 @@ export interface KubeService extends KubeObjectInterface { }; } -class Service extends makeKubeObject('service') { +class Service extends KubeObject { + static objectName = 'service'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'services'); get spec(): KubeService['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getExternalAddresses() { diff --git a/frontend/src/lib/k8s/serviceAccount.ts b/frontend/src/lib/k8s/serviceAccount.ts index fcfccf8969b..0ce6f3c5961 100644 --- a/frontend/src/lib/k8s/serviceAccount.ts +++ b/frontend/src/lib/k8s/serviceAccount.ts @@ -1,5 +1,5 @@ import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeServiceAccount extends KubeObjectInterface { secrets: { @@ -12,11 +12,12 @@ export interface KubeServiceAccount extends KubeObjectInterface { }[]; } -class ServiceAccount extends makeKubeObject('serviceAccount') { +class ServiceAccount extends KubeObject { + static objectName = 'serviceAccount'; static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'serviceaccounts'); get secrets(): KubeServiceAccount['secrets'] { - return this.jsonData!.secrets; + return this.jsonData.secrets; } } diff --git a/frontend/src/lib/k8s/statefulSet.ts b/frontend/src/lib/k8s/statefulSet.ts index 926b2227d31..2d181a38056 100644 --- a/frontend/src/lib/k8s/statefulSet.ts +++ b/frontend/src/lib/k8s/statefulSet.ts @@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy'; import { KubeContainer, KubeMetadata, + KubeObject, KubeObjectInterface, LabelSelector, - makeKubeObject, } from './cluster'; import { KubePodSpec } from './pod'; @@ -28,15 +28,16 @@ export interface KubeStatefulSet extends KubeObjectInterface { }; } -class StatefulSet extends makeKubeObject('StatefulSet') { +class StatefulSet extends KubeObject { + static objectName = 'StatefulSet'; static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'statefulsets', true); get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/storageClass.ts b/frontend/src/lib/k8s/storageClass.ts index 6f643f5cba9..fd9827c09fc 100644 --- a/frontend/src/lib/k8s/storageClass.ts +++ b/frontend/src/lib/k8s/storageClass.ts @@ -1,25 +1,31 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './cluster'; export interface KubeStorageClass extends KubeObjectInterface { provisioner: string; reclaimPolicy: string; volumeBindingMode: string; + allowVolumeExpansion?: boolean; } -class StorageClass extends makeKubeObject('storageClass') { +class StorageClass extends KubeObject { + static objectName = 'storageClass'; static apiEndpoint = apiFactory('storage.k8s.io', 'v1', 'storageclasses'); get provisioner() { - return this.jsonData?.provisioner; + return this.jsonData.provisioner; } get reclaimPolicy() { - return this.jsonData?.reclaimPolicy; + return this.jsonData.reclaimPolicy; } get volumeBindingMode() { - return this.jsonData?.volumeBindingMode; + return this.jsonData.volumeBindingMode; + } + + get allowVolumeExpansion() { + return this.jsonData.allowVolumeExpansion; } static get listRoute() { diff --git a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts index 576f0da99e8..1816bb58807 100644 --- a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts @@ -1,5 +1,5 @@ import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface, LabelSelector } from './cluster'; import { KubeRuleWithOperations, KubeWebhookClientConfig } from './mutatingWebhookConfiguration'; export interface KubeValidatingWebhookConfiguration extends KubeObjectInterface { @@ -23,9 +23,8 @@ export interface KubeValidatingWebhookConfiguration extends KubeObjectInterface }[]; } -class ValidatingWebhookConfiguration extends makeKubeObject( - 'ValidatingWebhookConfiguration' -) { +class ValidatingWebhookConfiguration extends KubeObject { + static objectName = 'ValidatingWebhookConfiguration'; static apiEndpoint = apiFactory( 'admissionregistration.k8s.io', 'v1', @@ -33,7 +32,7 @@ class ValidatingWebhookConfiguration extends makeKubeObject('verticalPodAutoscaler') { +class VPA extends KubeObject { + static objectName = 'verticalPodAutoscaler'; static apiEndpoint = apiFactoryWithNamespace( 'autoscaling.k8s.io', 'v1', @@ -102,11 +103,11 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { } get spec(): VpaSpec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): VpaStatus { - return this.jsonData!.status; + return this.jsonData.status; } get referenceObject(): KubeObject | null { @@ -115,7 +116,7 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { return null; } - const TargetObjectClass = ResourceClasses[target.kind]; + const TargetObjectClass = (ResourceClasses as Record)[target.kind]; let objInstance: KubeObject | null = null; if (!!TargetObjectClass) { objInstance = new TargetObjectClass({ @@ -124,7 +125,7 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { namespace: target.namespace || this.metadata.namespace, }, kind: target.kind, - }); + } as KubeObjectInterface); } return objInstance; } diff --git a/frontend/src/lib/util.ts b/frontend/src/lib/util.ts index 8406ab1d869..da69848410a 100644 --- a/frontend/src/lib/util.ts +++ b/frontend/src/lib/util.ts @@ -166,7 +166,7 @@ export function useFilterFunc< }; } -export function useErrorState(dependentSetter?: (...args: any) => void) { +export function useErrorState(dependentSetter?: (item: any) => void) { const [error, setError] = React.useState(null); React.useEffect( @@ -179,8 +179,7 @@ export function useErrorState(dependentSetter?: (...args: any) => void) { [error] ); - // Adding "as any" here because it was getting difficult to validate the setter type. - return [error, setError as any]; + return [error, setError] as const; } type URLStateParams = { diff --git a/frontend/src/plugin/registry.tsx b/frontend/src/plugin/registry.tsx index ba8b52b22aa..75bc97e7c64 100644 --- a/frontend/src/plugin/registry.tsx +++ b/frontend/src/plugin/registry.tsx @@ -14,7 +14,7 @@ import { DetailsViewSectionProps, DetailsViewSectionType } from '../components/D import { addDetailsViewSectionsProcessor, DefaultDetailsViewSection, - DetailsViewSectionsProcessor, + DetailsViewsSectionProcessor, setDetailsViewSection, } from '../components/DetailsViewSection/detailsViewSectionSlice'; import { DefaultSidebars, SidebarEntryProps } from '../components/Sidebar'; @@ -185,7 +185,7 @@ export default class Registry { */ registerDetailsViewSection( sectionName: string, - sectionFunc: (props: { resource: any }) => SectionFuncProps | null + sectionFunc: (resource: KubeObject) => SectionFuncProps | null ) { console.warn( 'Registry.registerDetailsViewSection is deprecated. Please use registerDetailsViewSection.' @@ -520,7 +520,7 @@ export function registerDetailsViewSection(viewSection: DetailsViewSectionType) * ``` */ export function registerDetailsViewSectionsProcessor( - processor: DetailsViewSectionsProcessor | DetailsViewSectionsProcessor['processor'] + processor: DetailsViewsSectionProcessor | DetailsViewsSectionProcessor['processor'] ) { store.dispatch(addDetailsViewSectionsProcessor(processor)); } diff --git a/frontend/src/test/index.tsx b/frontend/src/test/index.tsx index 35a65d0bd68..fa8b7042b92 100644 --- a/frontend/src/test/index.tsx +++ b/frontend/src/test/index.tsx @@ -2,7 +2,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter, Route } from 'react-router-dom'; -import { KubeObject } from '../lib/k8s/cluster'; import Event from '../lib/k8s/event'; import defaultStore from '../redux/stores/store'; @@ -71,12 +70,10 @@ export function TestContext(props: TestContextProps) { ); } -export function overrideKubeObject( - kubeObject: KubeObject, - propsToOverride: { [method: keyof KubeObject]: KubeObject[keyof KubeObject] | undefined } -) { +export function overrideKubeObject(kubeObject: U, propsToOverride: Partial) { for (const [key, value] of Object.entries(propsToOverride)) { if (value !== undefined) { + // @ts-ignore kubeObject[key] = value; } } diff --git a/frontend/src/test/mocker.ts b/frontend/src/test/mocker.ts index 7143f288088..ca3f8ce29c8 100644 --- a/frontend/src/test/mocker.ts +++ b/frontend/src/test/mocker.ts @@ -1,22 +1,30 @@ import _ from 'lodash'; -import { KubeObjectIface, KubeObjectInterface } from '../lib/k8s/cluster'; +import { KubeMetadata, KubeObject, KubeObjectClass, KubeObjectInterface } from '../lib/k8s/cluster'; -interface K8sResourceListGeneratorOptions { +interface K8sResourceListGeneratorOptions { numResults?: number; - instantiateAs?: KubeObjectIface; + instantiateAs?: T; } -export function generateK8sResourceList( - baseJson: Omit, - options: K8sResourceListGeneratorOptions = {} -) { +export function generateK8sResourceList< + C extends typeof KubeObject, + T extends KubeObjectInterface +>(baseJson: Partial, options?: { numResults?: number; instantiateAs: C }): InstanceType[]; +export function generateK8sResourceList( + baseJson: Partial, + options?: { numResults?: number } +): T[]; +export function generateK8sResourceList< + T extends KubeObjectInterface, + C extends typeof KubeObject +>(baseJson: Partial, options: K8sResourceListGeneratorOptions = {}) { const { numResults = 5, instantiateAs } = options; const list = []; for (let i = 0; i < numResults; i++) { const json = { metadata: { name: '', - }, + } as KubeMetadata, ..._.cloneDeep(baseJson), } as T;