Skip to content

Commit

Permalink
frontend: Refactor KubeObject class, deprecate makeKubeObject
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Dubenko <oldubenko@microsoft.com>
  • Loading branch information
sniok committed Aug 6, 2024
1 parent 90a38ac commit 9831c04
Show file tree
Hide file tree
Showing 110 changed files with 1,149 additions and 1,267 deletions.
226 changes: 20 additions & 206 deletions frontend/src/components/App/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -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 | HTMLElement>(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 (
<>
<IconButton
size="small"
onClick={event => {
setAnchorEl(event.currentTarget);
}}
aria-haspopup="menu"
aria-controls={menuId}
aria-label={t('Actions')}
>
<Icon icon="mdi:more-vert" />
</IconButton>
<Menu
id={menuId}
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={() => {
handleMenuClose();
}}
>
<MenuItem
onClick={() => {
history.push(createRouteURL('cluster', { cluster: cluster.name }));
handleMenuClose();
}}
>
<ListItemText>{t('translation|View')}</ListItemText>
</MenuItem>
<MenuItem
onClick={() => {
history.push(createRouteURL('settingsCluster', { cluster: cluster.name }));
handleMenuClose();
}}
>
<ListItemText>{t('translation|Settings')}</ListItemText>
</MenuItem>
{helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && (
<MenuItem
onClick={() => {
setOpenConfirmDialog(true);
handleMenuClose();
}}
>
<ListItemText>{t('translation|Delete')}</ListItemText>
</MenuItem>
)}
</Menu>

<ConfirmDialog
open={openConfirmDialog}
handleClose={() => 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 (
<Box width="fit-content">
<Box display="flex" alignItems="center" justifyContent="center">
{hasReachError ? (
<Icon icon="mdi:cloud-off" width={16} color={theme.palette.home.status.error} />
) : stateUnknown ? (
<Icon icon="mdi:cloud-question" width={16} color={theme.palette.home.status.unknown} />
) : (
<Icon
icon="mdi:cloud-check-variant"
width={16}
color={theme.palette.home.status.success}
/>
)}
<Typography
variant="body2"
style={{
marginLeft: theme.spacing(1),
color: hasReachError
? theme.palette.home.status.error
: !stateUnknown
? theme.palette.home.status.success
: undefined,
}}
>
{hasReachError ? error.message : stateUnknown ? '⋯' : t('translation|Active')}
</Typography>
</Box>
</Box>
);
}

export default function Home() {
const history = useHistory();
const clusters = useClustersConf() || {};
Expand All @@ -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 (
<PageGrid>
Expand All @@ -213,44 +64,7 @@ function HomeComponent(props: HomeComponentProps) {
/>
}
>
<ResourceTable
defaultSortingColumn={{ id: 'name', desc: false }}
columns={[
{
id: 'name',
label: t('Name'),
getValue: cluster => cluster.name,
render: ({ name }) => (
<Link routeName="cluster" params={{ cluster: name }}>
{name}
</Link>
),
},
{
label: t('Status'),
getValue: cluster => cluster.name,
render: ({ name }) => <ClusterStatus error={errors[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 => <ContextMenu cluster={cluster} />,
},
]}
data={Object.values(customNameClusters)}
id="headlamp-home-clusters"
/>
<ResourceTable id="headlamp-home-clusters" columns={[]} data={[]}></ResourceTable>
</SectionBox>
</PageGrid>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -63,10 +64,10 @@ const Template: Story<DetailsViewSectionProps> = 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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import detailsViewSectionReducer, {
addDetailsViewSectionsProcessor,
DefaultDetailsViewSection,
DetailsViewSection,
DetailsViewSectionProcessorType,
DetailsViewsSectionProcessor,
setDetailsView,
setDetailsViewSection,
Expand Down Expand Up @@ -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([
Expand All @@ -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);
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/components/cluster/Charts.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -82,7 +82,7 @@ export function CpuCircularChart(props: ResourceCircularChartProps) {
);
}

export function PodsStatusCircleChart(props: Pick<ResourceCircularChartProps, 'items'>) {
export function PodsStatusCircleChart(props: { items: Pod[] | null }) {
const theme = useTheme();
const { items } = props;
const { t } = useTranslation(['translation', 'glossary']);
Expand Down Expand Up @@ -143,7 +143,7 @@ export function PodsStatusCircleChart(props: Pick<ResourceCircularChartProps, 'i
);
}

export function NodesStatusCircleChart(props: Pick<ResourceCircularChartProps, 'items'>) {
export function NodesStatusCircleChart(props: { items: Node[] | null }) {
const theme = useTheme();
const { items } = props;
const { t } = useTranslation(['translation', 'glossary']);
Expand Down
11 changes: 4 additions & 7 deletions frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -49,13 +49,10 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, State>
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 ? <FallbackComponent error={error} /> : null;
}
}
2 changes: 1 addition & 1 deletion frontend/src/components/common/LabelListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 9831c04

Please sign in to comment.