feat: add errors to Pod Drawer (#17563)
* feat: add errors to Pod Drawer Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: lint fix Signed-off-by: Matthew Clarke <mclarke@spotify.com> --------- Signed-off-by: Matthew Clarke <mclarke@spotify.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes': patch
|
||||
---
|
||||
|
||||
Add errors to PodDrawer
|
||||
@@ -22,6 +22,7 @@ import { OAuthApi } from '@backstage/core-plugin-api';
|
||||
import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
|
||||
import { OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { Pod } from 'kubernetes-models/v1';
|
||||
import { Pod as Pod_2 } from 'kubernetes-models/v1/Pod';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { V1ConfigMap } from '@kubernetes/client-node';
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@backstage/plugin-kubernetes-common": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@kubernetes-models/apimachinery": "^1.1.0",
|
||||
"@kubernetes-models/base": "^4.0.1",
|
||||
"@kubernetes/client-node": "0.18.1",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Cluster } from './Cluster';
|
||||
import EmptyStateImage from '../assets/emptystate.svg';
|
||||
import { useKubernetesObjects } from '../hooks';
|
||||
import { Content, Page, Progress } from '@backstage/core-components';
|
||||
import { DetectedErrorsContext } from '../hooks/useMatchingErrors';
|
||||
|
||||
type KubernetesContentProps = {
|
||||
entity: Entity;
|
||||
@@ -49,88 +50,92 @@ export const KubernetesContent = ({
|
||||
: new Map<string, DetectedError[]>();
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Content>
|
||||
{kubernetesObjects === undefined && error === undefined && <Progress />}
|
||||
<DetectedErrorsContext.Provider value={[...detectedErrors.values()].flat()}>
|
||||
<Page themeId="tool">
|
||||
<Content>
|
||||
{kubernetesObjects === undefined && error === undefined && (
|
||||
<Progress />
|
||||
)}
|
||||
|
||||
{/* errors retrieved from the kubernetes clusters */}
|
||||
{clustersWithErrors.length > 0 && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
clustersWithErrors={clustersWithErrors}
|
||||
/>
|
||||
{/* errors retrieved from the kubernetes clusters */}
|
||||
{clustersWithErrors.length > 0 && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
clustersWithErrors={clustersWithErrors}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* other errors */}
|
||||
{error !== undefined && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
errorMessage={error}
|
||||
/>
|
||||
{/* other errors */}
|
||||
{error !== undefined && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorPanel
|
||||
entityName={entity.metadata.name}
|
||||
errorMessage={error}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
)}
|
||||
|
||||
{kubernetesObjects && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorReporting detectedErrors={detectedErrors} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h3">Your Clusters</Typography>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
{kubernetesObjects?.items.length <= 0 && (
|
||||
<Grid
|
||||
container
|
||||
justifyContent="space-around"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h5">
|
||||
No resources on any known clusters for{' '}
|
||||
{entity.metadata.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<img
|
||||
src={EmptyStateImage}
|
||||
alt="EmptyState"
|
||||
data-testid="emptyStateImg"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{kubernetesObjects?.items.length > 0 &&
|
||||
kubernetesObjects?.items.map((item, i) => {
|
||||
const podsWithErrors = new Set<string>(
|
||||
detectedErrors
|
||||
.get(item.cluster.name)
|
||||
?.filter(de => de.sourceRef.kind === 'Pod')
|
||||
.map(de => de.sourceRef.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid item key={i} xs={12}>
|
||||
<Cluster
|
||||
clusterObjects={item}
|
||||
podsWithErrors={podsWithErrors}
|
||||
{kubernetesObjects && (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<ErrorReporting detectedErrors={detectedErrors} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="h3">Your Clusters</Typography>
|
||||
</Grid>
|
||||
<Grid item container>
|
||||
{kubernetesObjects?.items.length <= 0 && (
|
||||
<Grid
|
||||
container
|
||||
justifyContent="space-around"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<Typography variant="h5">
|
||||
No resources on any known clusters for{' '}
|
||||
{entity.metadata.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<img
|
||||
src={EmptyStateImage}
|
||||
alt="EmptyState"
|
||||
data-testid="emptyStateImg"
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
)}
|
||||
{kubernetesObjects?.items.length > 0 &&
|
||||
kubernetesObjects?.items.map((item, i) => {
|
||||
const podsWithErrors = new Set<string>(
|
||||
detectedErrors
|
||||
.get(item.cluster.name)
|
||||
?.filter(de => de.sourceRef.kind === 'Pod')
|
||||
.map(de => de.sourceRef.name),
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid item key={i} xs={12}>
|
||||
<Cluster
|
||||
clusterObjects={item}
|
||||
podsWithErrors={podsWithErrors}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Content>
|
||||
</Page>
|
||||
)}
|
||||
</Content>
|
||||
</Page>
|
||||
</DetectedErrorsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { ErrorList } from './ErrorList';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
|
||||
describe('ErrorList', () => {
|
||||
it('error highlight should render', () => {
|
||||
const { getByText } = render(
|
||||
<ErrorList
|
||||
podAndErrors={[
|
||||
{
|
||||
clusterName: 'some-cluster',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
} as Pod,
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occuranceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
possibleFixes: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('some-pod')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Paper,
|
||||
} from '@material-ui/core';
|
||||
import { PodAndErrors } from '../types';
|
||||
|
||||
const useStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
overflow: 'auto',
|
||||
},
|
||||
list: {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface ErrorListProps {
|
||||
podAndErrors: PodAndErrors[];
|
||||
}
|
||||
|
||||
export const ErrorList = ({ podAndErrors }: ErrorListProps) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<List className={classes.list}>
|
||||
{podAndErrors
|
||||
.filter(pae => pae.errors.length > 0)
|
||||
.flatMap(onlyPodWithErrors => {
|
||||
return onlyPodWithErrors.errors.map((error, i) => {
|
||||
return (
|
||||
<React.Fragment
|
||||
key={`${
|
||||
onlyPodWithErrors.pod.metadata?.name ?? 'unknown'
|
||||
}-eli-${i}`}
|
||||
>
|
||||
{i > 0 && <Divider key={`error-divider${i}`} />}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={error.message}
|
||||
secondary={onlyPodWithErrors.pod.metadata?.name}
|
||||
/>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export * from './ErrorList';
|
||||
@@ -33,7 +33,7 @@ describe('PodDrawer', () => {
|
||||
clusterName: 'some-cluster-1',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'ok-pod',
|
||||
name: 'some-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
@@ -51,17 +51,40 @@ describe('PodDrawer', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: [],
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occuranceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
possibleFixes: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getAllByText('ok-pod')).toHaveLength(2);
|
||||
expect(getAllByText('some-pod')).toHaveLength(3);
|
||||
expect(getByText('Pod (127.0.0.1)')).toBeInTheDocument();
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Containers')).toBeInTheDocument();
|
||||
expect(getByText('some-container')).toBeInTheDocument();
|
||||
expect(getByText('some error message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import { ContainerCard } from './ContainerCard';
|
||||
import { PodAndErrors } from '../types';
|
||||
import { KubernetesDrawer } from '../../KubernetesDrawer';
|
||||
import { PendingPodContent } from './PendingPodContent';
|
||||
import { ErrorList } from '../ErrorList';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -116,6 +117,16 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
|
||||
)}
|
||||
</ItemCardGrid>
|
||||
</Grid>
|
||||
{podAndErrors.errors.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Errors:</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{podAndErrors.errors.length > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<ErrorList podAndErrors={[podAndErrors]} />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+33
-22
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { V1Pod } from '@kubernetes/client-node';
|
||||
import { PodDrawer } from './PodDrawer';
|
||||
import {
|
||||
containersReady,
|
||||
@@ -27,18 +26,21 @@ import {
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics';
|
||||
import { ClusterContext } from '../../hooks/Cluster';
|
||||
import { useMatchingErrors } from '../../hooks/useMatchingErrors';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
import { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
export const READY_COLUMNS: PodColumns = 'READY';
|
||||
export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE';
|
||||
export type PodColumns = 'READY' | 'RESOURCE';
|
||||
|
||||
type PodsTablesProps = {
|
||||
pods: V1Pod[];
|
||||
pods: Pod | V1Pod[];
|
||||
extraColumns?: PodColumns[];
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const READY: TableColumn<V1Pod>[] = [
|
||||
const READY: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'containers ready',
|
||||
align: 'center',
|
||||
@@ -54,26 +56,37 @@ const READY: TableColumn<V1Pod>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const PodDrawerTrigger = ({ pod }: { pod: Pod }) => {
|
||||
const cluster = useContext(ClusterContext);
|
||||
const errors = useMatchingErrors({
|
||||
kind: 'Pod',
|
||||
apiVersion: 'v1',
|
||||
metadata: pod.metadata,
|
||||
});
|
||||
return (
|
||||
<PodDrawer
|
||||
podAndErrors={{
|
||||
pod: pod as any,
|
||||
clusterName: cluster.name,
|
||||
errors: errors,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
const podNamesWithMetrics = useContext(PodNamesWithMetricsContext);
|
||||
const cluster = useContext(ClusterContext);
|
||||
const defaultColumns: TableColumn<V1Pod>[] = [
|
||||
const defaultColumns: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'name',
|
||||
highlight: true,
|
||||
render: (pod: V1Pod) => (
|
||||
<PodDrawer
|
||||
podAndErrors={{
|
||||
pod: pod as any,
|
||||
clusterName: cluster.name,
|
||||
errors: [],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
render: (pod: Pod) => {
|
||||
return <PodDrawerTrigger pod={pod} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'phase',
|
||||
render: (pod: V1Pod) => pod.status?.phase ?? 'unknown',
|
||||
render: (pod: Pod) => pod.status?.phase ?? 'unknown',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
@@ -81,16 +94,16 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
render: containerStatuses,
|
||||
},
|
||||
];
|
||||
const columns: TableColumn<V1Pod>[] = [...defaultColumns];
|
||||
const columns: TableColumn<Pod>[] = [...defaultColumns];
|
||||
|
||||
if (extraColumns.includes(READY_COLUMNS)) {
|
||||
columns.push(...READY);
|
||||
}
|
||||
if (extraColumns.includes(RESOURCE_COLUMNS)) {
|
||||
const resourceColumns: TableColumn<V1Pod>[] = [
|
||||
const resourceColumns: TableColumn<Pod>[] = [
|
||||
{
|
||||
title: 'CPU usage %',
|
||||
render: (pod: V1Pod) => {
|
||||
render: (pod: Pod) => {
|
||||
const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? '');
|
||||
|
||||
if (!metrics) {
|
||||
@@ -103,7 +116,7 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
},
|
||||
{
|
||||
title: 'Memory usage %',
|
||||
render: (pod: V1Pod) => {
|
||||
render: (pod: Pod) => {
|
||||
const metrics = podNamesWithMetrics.get(pod.metadata?.name ?? '');
|
||||
|
||||
if (!metrics) {
|
||||
@@ -123,13 +136,11 @@ export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const usePods = pods.map(p => ({ ...p, id: p.metadata?.uid }));
|
||||
|
||||
return (
|
||||
<div style={tableStyle}>
|
||||
<Table
|
||||
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
|
||||
data={usePods}
|
||||
data={pods as Pod[]}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Pod } from 'kubernetes-models/v1';
|
||||
import { DetectedError } from '../../error-detection';
|
||||
|
||||
export interface PodAndErrors {
|
||||
clusterName: string;
|
||||
pod: Pod;
|
||||
errors: any[];
|
||||
errors: DetectedError[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { DetectedErrorsContext, useMatchingErrors } from './useMatchingErrors';
|
||||
import { DetectedError } from '../error-detection';
|
||||
import { ResourceRef } from '../error-detection/types';
|
||||
|
||||
const genericErrorWithRef = (resourceRef: ResourceRef): DetectedError => {
|
||||
return {
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occuranceCount: 1,
|
||||
sourceRef: resourceRef,
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
possibleFixes: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
describe('useMatchingErrors', () => {
|
||||
it('should filter non-matching resources', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<DetectedErrorsContext.Provider
|
||||
value={[
|
||||
genericErrorWithRef({
|
||||
name: 'some-other-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'some-kind',
|
||||
apiGroup: 'some-apiGroup',
|
||||
}),
|
||||
genericErrorWithRef({
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'some-kind',
|
||||
apiGroup: 'some-apiGroup',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</DetectedErrorsContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useMatchingErrors({
|
||||
metadata: {
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
kind: 'some-kind',
|
||||
apiVersion: 'some-apiGroup',
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
expect(result.current).toStrictEqual([
|
||||
genericErrorWithRef({
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'some-kind',
|
||||
apiGroup: 'some-apiGroup',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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, { useContext } from 'react';
|
||||
import { DetectedError, ResourceRef } from '../error-detection/types';
|
||||
import { TypeMeta } from '@kubernetes-models/base';
|
||||
import { IIoK8sApimachineryPkgApisMetaV1ObjectMeta as V1ObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
|
||||
|
||||
export const DetectedErrorsContext = React.createContext<DetectedError[]>([]);
|
||||
|
||||
type Matcher = {
|
||||
metadata?: V1ObjectMeta;
|
||||
} & TypeMeta;
|
||||
|
||||
export const useMatchingErrors = (matcher: Matcher): DetectedError[] => {
|
||||
const targetRef: ResourceRef = {
|
||||
name: matcher.metadata?.name ?? '',
|
||||
namespace: matcher.metadata?.namespace ?? '',
|
||||
kind: matcher.kind,
|
||||
apiGroup: matcher.apiVersion,
|
||||
};
|
||||
|
||||
const errors = useContext(DetectedErrorsContext);
|
||||
|
||||
return errors.filter(e => {
|
||||
const r = e.sourceRef;
|
||||
return (
|
||||
targetRef.apiGroup === r.apiGroup &&
|
||||
targetRef.kind === r.kind &&
|
||||
targetRef.name === r.name &&
|
||||
targetRef.namespace === r.namespace
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SubvalueCell,
|
||||
} from '@backstage/core-components';
|
||||
import { ClientPodStatus } from '@backstage/plugin-kubernetes-common';
|
||||
import { Pod } from 'kubernetes-models/v1/Pod';
|
||||
|
||||
export const imageChips = (pod: V1Pod): ReactNode => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
@@ -38,19 +39,19 @@ export const imageChips = (pod: V1Pod): ReactNode => {
|
||||
return <div>{images}</div>;
|
||||
};
|
||||
|
||||
export const containersReady = (pod: V1Pod): string => {
|
||||
export const containersReady = (pod: Pod): string => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
const containersReadyItem = containerStatuses.filter(cs => cs.ready).length;
|
||||
|
||||
return `${containersReadyItem}/${containerStatuses.length}`;
|
||||
};
|
||||
|
||||
export const totalRestarts = (pod: V1Pod): number => {
|
||||
export const totalRestarts = (pod: Pod): number => {
|
||||
const containerStatuses = pod.status?.containerStatuses ?? [];
|
||||
return containerStatuses?.reduce((a, b) => a + b.restartCount, 0);
|
||||
};
|
||||
|
||||
export const containerStatuses = (pod: V1Pod): ReactNode => {
|
||||
export const containerStatuses = (pod: Pod): ReactNode => {
|
||||
const containerStatusesItem = pod.status?.containerStatuses ?? [];
|
||||
const errors = containerStatusesItem.reduce((accum, next) => {
|
||||
if (next.state === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user