add better error handling (#2664)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ErrorPanel } from './ErrorPanel';
|
||||
|
||||
describe('ErrorPanel', () => {
|
||||
it('render with error message', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ErrorPanel
|
||||
entityName="THIS_ENTITY"
|
||||
errorMessage="SOME_ERROR_MESSAGE"
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(
|
||||
getByText(
|
||||
'There was an error retrieving some Kubernetes resources for the entity: THIS_ENTITY',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// message
|
||||
expect(getByText('Errors: SOME_ERROR_MESSAGE')).toBeInTheDocument();
|
||||
});
|
||||
it('render with cluster errors', async () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(
|
||||
<ErrorPanel
|
||||
entityName="THIS_ENTITY"
|
||||
clustersWithErrors={[
|
||||
{
|
||||
cluster: {
|
||||
name: 'THIS_CLUSTER',
|
||||
},
|
||||
resources: [],
|
||||
errors: [
|
||||
{
|
||||
errorType: 'SYSTEM_ERROR',
|
||||
statusCode: 500,
|
||||
resourcePath: 'some/resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
// title
|
||||
expect(
|
||||
getByText(
|
||||
'There was an error retrieving some Kubernetes resources for the entity: THIS_ENTITY',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// message
|
||||
expect(getByText('Errors:')).toBeInTheDocument();
|
||||
expect(getByText('Cluster: THIS_CLUSTER')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
"Error fetching Kubernetes resource: 'some/resource', error: SYSTEM_ERROR",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { ClusterObjects } from '../../../../kubernetes-backend/src';
|
||||
|
||||
const clustersWithErrorsToErrorMessage = (
|
||||
clustersWithErrors: ClusterObjects[],
|
||||
): React.ReactNode => {
|
||||
return clustersWithErrors.map((c, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Typography variant="body2">{`Cluster: ${c.cluster.name}`}</Typography>
|
||||
{c.errors.map((e, j) => {
|
||||
return (
|
||||
<Typography variant="body2" key={j}>
|
||||
{`Error fetching Kubernetes resource: '${e.resourcePath}', error: ${e.errorType}`}
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
type ErrorPanelProps = {
|
||||
entityName: string;
|
||||
errorMessage?: string;
|
||||
clustersWithErrors?: ClusterObjects[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ErrorPanel = ({
|
||||
entityName,
|
||||
errorMessage,
|
||||
clustersWithErrors,
|
||||
}: ErrorPanelProps) => (
|
||||
<WarningPanel
|
||||
title="There was an error retrieving kubernetes objects"
|
||||
message={`There was an error retrieving some Kubernetes resources for the entity: ${entityName}`}
|
||||
>
|
||||
{clustersWithErrors && (
|
||||
<div>Errors: {clustersWithErrorsToErrorMessage(clustersWithErrors)}</div>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<Typography variant="body2">Errors: {errorMessage}</Typography>
|
||||
)}
|
||||
</WarningPanel>
|
||||
);
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { Grid, TabProps } from '@material-ui/core';
|
||||
import {
|
||||
CardTab,
|
||||
Content,
|
||||
@@ -44,6 +44,7 @@ import { Services } from '../Services';
|
||||
import { ConfigMaps } from '../ConfigMaps';
|
||||
import { Ingresses } from '../Ingresses';
|
||||
import { HorizontalPodAutoscalers } from '../HorizontalPodAutoscalers';
|
||||
import { ErrorPanel } from './ErrorPanel';
|
||||
|
||||
interface GroupedResponses extends DeploymentTriple {
|
||||
services: V1Service[];
|
||||
@@ -94,8 +95,6 @@ const groupResponses = (fetchResponse: FetchResponse[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
// TODO proper error handling
|
||||
|
||||
type KubernetesContentProps = { entity: Entity; children?: React.ReactNode };
|
||||
|
||||
export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
@@ -117,12 +116,33 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
});
|
||||
}, [entity.metadata.name, kubernetesApi]);
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Content>
|
||||
<Grid container spacing={3} direction="column">
|
||||
{kubernetesObjects === undefined && <Progress />}
|
||||
{error !== undefined && <div>{error}</div>}
|
||||
{kubernetesObjects === undefined && error === undefined && (
|
||||
<Progress />
|
||||
)}
|
||||
|
||||
{/* errors retrieved from the kubernetes clusters */}
|
||||
{clustersWithErrors.length > 0 && (
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
clustersWithErrors={clustersWithErrors}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* other errors */}
|
||||
{error !== undefined && (
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
errorMessage={error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{kubernetesObjects?.items.map((item, i) => (
|
||||
<Grid item key={i}>
|
||||
<Cluster clusterObjects={item} />
|
||||
@@ -151,6 +171,43 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
|
||||
const hpas = groupedResponses.horizontalPodAutoscalers;
|
||||
const ingresses = groupedResponses.ingresses;
|
||||
|
||||
const tabs: ReactElement<TabProps>[] = [
|
||||
<CardTab key={1} value="one" label="Deployments">
|
||||
<DeploymentTables
|
||||
deploymentTriple={{
|
||||
deployments: groupedResponses.deployments,
|
||||
replicaSets: groupedResponses.replicaSets,
|
||||
pods: groupedResponses.pods,
|
||||
}}
|
||||
/>
|
||||
</CardTab>,
|
||||
<CardTab key={2} value="two" label="Services">
|
||||
<Services services={groupedResponses.services} />
|
||||
</CardTab>,
|
||||
];
|
||||
|
||||
if (configMaps.length > 0) {
|
||||
tabs.push(
|
||||
<CardTab key={3} value="three" label="Config Maps">
|
||||
<ConfigMaps configMaps={configMaps} />
|
||||
</CardTab>,
|
||||
);
|
||||
}
|
||||
if (hpas.length > 0) {
|
||||
tabs.push(
|
||||
<CardTab key={4} value="four" label="Horizontal Pod Autoscalers">
|
||||
<HorizontalPodAutoscalers hpas={hpas} />
|
||||
</CardTab>,
|
||||
);
|
||||
}
|
||||
if (ingresses.length > 0) {
|
||||
tabs.push(
|
||||
<CardTab key={5} value="five" label="Ingresses">
|
||||
<Ingresses ingresses={ingresses} />
|
||||
</CardTab>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabbedCard
|
||||
@@ -158,33 +215,7 @@ const Cluster = ({ clusterObjects }: ClusterProps) => {
|
||||
onChange={handleChange}
|
||||
title={clusterObjects.cluster.name}
|
||||
>
|
||||
<CardTab value="one" label="Deployments">
|
||||
<DeploymentTables
|
||||
deploymentTriple={{
|
||||
deployments: groupedResponses.deployments,
|
||||
replicaSets: groupedResponses.replicaSets,
|
||||
pods: groupedResponses.pods,
|
||||
}}
|
||||
/>
|
||||
</CardTab>
|
||||
<CardTab value="two" label="Services">
|
||||
<Services services={groupedResponses.services} />
|
||||
</CardTab>
|
||||
{configMaps && (
|
||||
<CardTab value="three" label="Config Maps">
|
||||
<ConfigMaps configMaps={configMaps} />
|
||||
</CardTab>
|
||||
)}
|
||||
{hpas && (
|
||||
<CardTab value="four" label="Horizontal Pod Autoscalers">
|
||||
<HorizontalPodAutoscalers hpas={hpas} />
|
||||
</CardTab>
|
||||
)}
|
||||
{ingresses && (
|
||||
<CardTab value="five" label="Ingresses">
|
||||
<Ingresses ingresses={ingresses} />
|
||||
</CardTab>
|
||||
)}
|
||||
{tabs}
|
||||
</TabbedCard>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user