Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend: Improve error handling in list pages #2771

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ApiError } from '../../lib/k8s/api/v2/ApiError';
import { TestContext } from '../../test';
import {
ClusterGroupErrorMessage,
ClusterGroupErrorMessageProps,
} from './ClusterGroupErrorMessage';

const meta: Meta<typeof ClusterGroupErrorMessage> = {
component: ClusterGroupErrorMessage,
decorators: [
Story => (
<TestContext>
<Story />
</TestContext>
),
],
};

export default meta;
type Story = StoryObj<ClusterGroupErrorMessageProps>;

export const WithClusterErrors: Story = {
args: {
clusterErrors: {
cluster1: 'Error in cluster 1',
cluster3: 'Error in cluster 3',
},
errors: [
new ApiError('Error in cluster 1', { cluster: 'cluster1' }),
new ApiError('Error in cluster 3', { cluster: 'cluster3' }),
],
},
};

export const WithMessageUsed: Story = {
export const WithMutipleErrorsPerCluster: Story = {
args: {
message: 'This message is used and not clusterErrors.',
clusterErrors: {
cluster1: 'Error in cluster 1',
cluster3: 'Error in cluster 3',
},
},
};

export const WithDetailedClusterErrors: Story = {
args: {
clusterErrors: {
cluster1: 'Error in cluster 1',
cluster3: null,
},
errors: [
new ApiError('Error A in cluster 1', { cluster: 'cluster1' }),
new ApiError('Error B in cluster 1', { cluster: 'cluster1' }),
],
},
};
95 changes: 49 additions & 46 deletions frontend/src/components/cluster/ClusterGroupErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,66 @@
import { InlineIcon } from '@iconify/react';
import { Box, Typography, useTheme } from '@mui/material';
import { Alert, AlertTitle, Box, Button } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ApiError } from '../../lib/k8s/apiProxy';
import { useClusterGroup } from '../../lib/k8s';
import { ApiError } from '../../lib/k8s/api/v2/ApiError';

export interface ClusterGroupErrorMessageProps {
/**
* A message to display when clusters fail to load resources.
* This is used if it is passed in, otherwise a message made from clusterErrors is used.
* Array of errors
*/
message?: string;
/**
* Either an array of errors, or keyed by cluster name, valued by the error for that cluster.
*/
clusterErrors?: { [cluster: string]: string | ApiError | null };
errors?: ApiError[] | null;
}

/**
* filter out null errors
* @returns errors, but without any that have null values.
*/
function cleanNullErrors(errors: ClusterGroupErrorMessageProps['clusterErrors']) {
if (!errors) {
return {};
export function ClusterGroupErrorMessage({ errors }: ClusterGroupErrorMessageProps) {
if (!errors || errors?.length === 0) {
return null;
}
const cleanedErrors: ClusterGroupErrorMessageProps['clusterErrors'] = {};
Object.entries(errors).forEach(([cluster, error]) => {
if (error !== null) {
cleanedErrors[cluster] = error;
}
});

return cleanedErrors;

return errors.map((error, i) => <ErrorMessage error={error} key={error.stack ?? i} />);
}

export function ClusterGroupErrorMessage({
clusterErrors,
message,
}: ClusterGroupErrorMessageProps) {
function ErrorMessage({ error }: { error: ApiError }) {
const { t } = useTranslation();
const theme = useTheme();
const clusterObj = cleanNullErrors(typeof clusterErrors === 'object' ? clusterErrors : {});
const showClusterName = useClusterGroup().length > 1;
const [showMessage, setShowMessage] = useState(false);

if ((!clusterErrors && !message) || Object.keys(clusterObj).length === 0) {
return null;
const defaultTitle = t('Failed to load resources');
const forbiddenTitle = t("You don't have permission to view this resource");
const notFoundTitile = t('Resource not found');

const isForbidden = error.status === 403;

let title = defaultTitle;
if (error.status === 404) {
title = notFoundTitile;
} else if (isForbidden) {
title = forbiddenTitle;
}

const severity = isForbidden ? 'info' : 'warning';

return (
<Box p={1} style={{ background: theme.palette.warning.light }}>
<Typography style={{ color: theme.palette.warning.main }}>
<InlineIcon icon="mdi:alert" color={theme.palette.warning.main} />
&nbsp;
<>{message}</>
{!message &&
(Object.keys(clusterObj).length > 2
? t('Failed to load resources from some of the clusters in the group.')
: t('Failed to load resources from the following clusters: {{ clusterList }}', {
clusterList: Object.keys(clusterObj).join(', '),
}))}
</Typography>
</Box>
<Alert
severity={severity}
sx={{ mb: 1 }}
action={
<Button
size="small"
color={severity}
onClick={() => setShowMessage(it => !it)}
sx={{ whiteSpace: 'nowrap' }}
>
{showMessage ? t('Hide details') : t('Show details')}
</Button>
}
>
<AlertTitle sx={{ mb: showMessage ? undefined : 0 }}>{title}</AlertTitle>
{showMessage && (
<>
{showClusterName ? <Box>Cluster: {error.cluster}</Box> : null}
{error.message}
</>
)}
</Alert>
);
}
8 changes: 4 additions & 4 deletions frontend/src/components/cluster/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Node from '../../lib/k8s/node';
import Pod from '../../lib/k8s/pod';
import { useFilterFunc } from '../../lib/util';
import { DateLabel, Link, PageGrid, StatusLabel } from '../common';
import Empty from '../common/EmptyContent';
import ResourceListView from '../common/Resource/ResourceListView';
import { SectionBox } from '../common/SectionBox';
import ShowHideLabel from '../common/ShowHideLabel';
Expand All @@ -19,6 +18,7 @@ import {
NodesStatusCircleChart,
PodsStatusCircleChart,
} from './Charts';
import { ClusterGroupErrorMessage } from './ClusterGroupErrorMessage';

export default function Overview() {
const { t } = useTranslation(['translation']);
Expand All @@ -35,7 +35,7 @@ export default function Overview() {
<PageGrid>
<SectionBox title={t('translation|Overview')} py={2} mt={[4, 0, 0]}>
{noPermissions ? (
<Empty color="error">{t('translation|No permissions to list pods.')}</Empty>
<ClusterGroupErrorMessage errors={[metricsError]} />
) : (
<Grid container justifyContent="flex-start" alignItems="stretch" spacing={4}>
<Grid item xs sx={{ maxWidth: '300px' }}>
Expand Down Expand Up @@ -74,7 +74,7 @@ function EventsSection() {
)
)
);
const [events, eventsError] = Event.useList({ limit: Event.maxLimit });
const { items: events, errors: eventsErrors } = Event.useList({ limit: Event.maxLimit });

const warningActionFilterFunc = (event: Event, search?: string) => {
if (!filterFunc(event, search)) {
Expand Down Expand Up @@ -138,7 +138,7 @@ function EventsSection() {
}}
defaultGlobalFilter={eventsFilter ?? undefined}
data={events}
errorMessage={Event.getErrorMessage(eventsError)}
errors={eventsErrors}
columns={[
{
label: t('Type'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,90 @@
<body>
<div>
<div
class="MuiBox-root css-hpgf8j"
style="background: rgb(255, 243, 224);"
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorWarning MuiAlert-standardWarning MuiAlert-standard css-18jeh35-MuiPaper-root-MuiAlert-root"
role="alert"
>
<p
class="MuiTypography-root MuiTypography-body1 css-1ezega9-MuiTypography-root"
style="color: rgb(196, 69, 0);"
<div
class="MuiAlert-icon css-1ytlwq5-MuiAlert-icon"
>

Failed to load resources from the following clusters: cluster1, cluster3
</p>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit css-1vooibu-MuiSvgIcon-root"
data-testid="ReportProblemOutlinedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"
/>
</svg>
</div>
<div
class="MuiAlert-message css-1pxa9xg-MuiAlert-message"
>
<div
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom MuiAlertTitle-root css-m946el-MuiTypography-root-MuiAlertTitle-root"
>
Failed to load resources
</div>
</div>
<div
class="MuiAlert-action css-ki1hdl-MuiAlert-action"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textWarning MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorWarning MuiButton-root MuiButton-text MuiButton-textWarning MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorWarning css-wucs85-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Show details
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-rounded MuiPaper-elevation0 MuiAlert-root MuiAlert-colorWarning MuiAlert-standardWarning MuiAlert-standard css-18jeh35-MuiPaper-root-MuiAlert-root"
role="alert"
>
<div
class="MuiAlert-icon css-1ytlwq5-MuiAlert-icon"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeInherit css-1vooibu-MuiSvgIcon-root"
data-testid="ReportProblemOutlinedIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"
/>
</svg>
</div>
<div
class="MuiAlert-message css-1pxa9xg-MuiAlert-message"
>
<div
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom MuiAlertTitle-root css-m946el-MuiTypography-root-MuiAlertTitle-root"
>
Failed to load resources
</div>
</div>
<div
class="MuiAlert-action css-ki1hdl-MuiAlert-action"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textWarning MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorWarning MuiButton-root MuiButton-text MuiButton-textWarning MuiButton-sizeSmall MuiButton-textSizeSmall MuiButton-colorWarning css-wucs85-MuiButtonBase-root-MuiButton-root"
tabindex="0"
type="button"
>
Show details
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</body>

This file was deleted.

This file was deleted.

Loading
Loading