feat: add Kubernetes pod logs component (#17120)
* refactor: rename kubernetes structured metadata table Signed-off-by: Matthew Clarke <mclarke@spotify.com> * feat: add pod logs components Signed-off-by: Matthew Clarke <mclarke@spotify.com> * refactor: more generic proxy token generating Signed-off-by: Matthew Clarke <mclarke@spotify.com> * tests Signed-off-by: Matthew Clarke <mclarke@spotify.com> * chore: changeset Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: tsc fix Signed-off-by: Matthew Clarke <mclarke@spotify.com> * docs: api report Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: remove event/errors mention Signed-off-by: Matthew Clarke <mclarke@spotify.com> * feat: pending pod conditions Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: missed file Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: merge master Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: remove unneeded code Signed-off-by: Matthew Clarke <mclarke@spotify.com> * feat: add proxy client Signed-off-by: Matthew Clarke <mclarke@spotify.com> * docs: breaking change Signed-off-by: Matthew Clarke <mclarke@spotify.com> * Update plugins/kubernetes/src/api/types.ts Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Matthew Clarke <matthewclarke47@gmail.com> * Update plugins/kubernetes/src/api/types.ts Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Matthew Clarke <matthewclarke47@gmail.com> * fix: PR feedback Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: test Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: PR feedback Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: PR feedback Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: todo Signed-off-by: Matthew Clarke <mclarke@spotify.com> * fix: api reports Signed-off-by: Matthew Clarke <mclarke@spotify.com> --------- Signed-off-by: Matthew Clarke <mclarke@spotify.com> Signed-off-by: Matthew Clarke <matthewclarke47@gmail.com> Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { KubernetesProxyClient } from './KubernetesProxyClient';
|
||||
|
||||
describe('KubernetesProxyClient', () => {
|
||||
let proxy: KubernetesProxyClient;
|
||||
const callProxyMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
proxy = new KubernetesProxyClient({
|
||||
kubernetesApi: {
|
||||
proxy: callProxyMock,
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
it('/logs returns log text', async () => {
|
||||
const request = {
|
||||
podName: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
containerName: 'some-container',
|
||||
};
|
||||
|
||||
callProxyMock.mockResolvedValue({
|
||||
text: jest.fn().mockResolvedValue('Hello World'),
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const response = await proxy.getPodLogs(request);
|
||||
await expect(response).toStrictEqual({ text: 'Hello World' });
|
||||
expect(callProxyMock).toHaveBeenCalledWith({
|
||||
clusterName: 'some-cluster',
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
path: '/api/v1/namespaces/some-namespace/pods/some-pod/log?container=some-container',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 { KubernetesApi } from './types';
|
||||
|
||||
/**
|
||||
* A client for common requests through the proxy endpoint of the kubernetes backend plugin.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class KubernetesProxyClient {
|
||||
private readonly kubernetesApi: KubernetesApi;
|
||||
|
||||
constructor(options: { kubernetesApi: KubernetesApi }) {
|
||||
this.kubernetesApi = options.kubernetesApi;
|
||||
}
|
||||
|
||||
private async handleText(response: Response): Promise<string> {
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
let message;
|
||||
switch (response.status) {
|
||||
default:
|
||||
message = `Proxy request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
async getPodLogs({
|
||||
podName,
|
||||
namespace,
|
||||
clusterName,
|
||||
containerName,
|
||||
}: {
|
||||
podName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
}): Promise<{ text: string }> {
|
||||
const params = new URLSearchParams({
|
||||
container: containerName,
|
||||
});
|
||||
return await this.kubernetesApi
|
||||
.proxy({
|
||||
clusterName: clusterName,
|
||||
path: `/api/v1/namespaces/${namespace}/pods/${podName}/log?${params.toString()}`,
|
||||
init: {
|
||||
method: 'GET',
|
||||
},
|
||||
})
|
||||
.then(response => this.handleText(response))
|
||||
.then(text => ({ text }));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { kubernetesApiRef } from './types';
|
||||
export type { KubernetesApi } from './types';
|
||||
export { kubernetesApiRef, kubernetesProxyApiRef } from './types';
|
||||
export type { KubernetesApi, KubernetesProxyApi } from './types';
|
||||
export { KubernetesBackendClient } from './KubernetesBackendClient';
|
||||
export { KubernetesProxyClient } from './KubernetesProxyClient';
|
||||
|
||||
@@ -26,6 +26,10 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
id: 'plugin.kubernetes.service',
|
||||
});
|
||||
|
||||
export const kubernetesProxyApiRef = createApiRef<KubernetesProxyApi>({
|
||||
id: 'plugin.kubernetes.proxy-service',
|
||||
});
|
||||
|
||||
export interface KubernetesApi {
|
||||
getObjectsByEntity(
|
||||
requestBody: KubernetesRequestBody,
|
||||
@@ -49,3 +53,12 @@ export interface KubernetesApi {
|
||||
init?: RequestInit;
|
||||
}): Promise<Response>;
|
||||
}
|
||||
|
||||
export interface KubernetesProxyApi {
|
||||
getPodLogs(request: {
|
||||
podName: string;
|
||||
namespace: string;
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
}): Promise<{ text: string }>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { V1CronJob } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
export const CronJobDrawer = ({
|
||||
@@ -27,7 +27,7 @@ export const CronJobDrawer = ({
|
||||
}) => {
|
||||
const namespace = cronJob.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={cronJob}
|
||||
expanded={expanded}
|
||||
kind="CronJob"
|
||||
@@ -62,6 +62,6 @@ export const CronJobDrawer = ({
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { KubernetesDrawer } from '../../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const RolloutDrawer = ({
|
||||
@@ -26,7 +26,7 @@ export const RolloutDrawer = ({
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={rollout}
|
||||
expanded={expanded}
|
||||
kind="Rollout"
|
||||
@@ -50,6 +50,6 @@ export const RolloutDrawer = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
const capitalize = (str: string) =>
|
||||
@@ -33,7 +33,7 @@ export const DefaultCustomResourceDrawer = ({
|
||||
const capitalizedName = capitalize(customResourceName);
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={customResource}
|
||||
expanded={expanded}
|
||||
kind={capitalizedName}
|
||||
@@ -57,6 +57,6 @@ export const DefaultCustomResourceDrawer = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { V1Deployment } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { renderCondition } from '../../utils/pod';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DeploymentDrawer = ({
|
||||
}) => {
|
||||
const namespace = deployment.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={deployment}
|
||||
expanded={expanded}
|
||||
kind="Deployment"
|
||||
@@ -73,6 +73,6 @@ export const DeploymentDrawer = ({
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { V1HorizontalPodAutoscaler } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
|
||||
/** @public */
|
||||
export const HorizontalPodAutoscalerDrawer = (props: {
|
||||
@@ -27,7 +27,7 @@ export const HorizontalPodAutoscalerDrawer = (props: {
|
||||
const { hpa, expanded, children } = props;
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
kind="HorizontalPodAutoscaler"
|
||||
object={hpa}
|
||||
expanded={expanded}
|
||||
@@ -45,6 +45,6 @@ export const HorizontalPodAutoscalerDrawer = (props: {
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { V1Ingress } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const IngressDrawer = ({
|
||||
@@ -27,7 +27,7 @@ export const IngressDrawer = ({
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={ingress}
|
||||
expanded={expanded}
|
||||
kind="Ingress"
|
||||
@@ -53,6 +53,6 @@ export const IngressDrawer = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { V1Job } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const JobDrawer = ({
|
||||
@@ -26,7 +26,7 @@ export const JobDrawer = ({
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={job}
|
||||
expanded={expanded}
|
||||
kind="Job"
|
||||
@@ -57,6 +57,6 @@ export const JobDrawer = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* 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.
|
||||
@@ -13,36 +13,115 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { ChangeEvent, useState } from 'react';
|
||||
|
||||
import React, { ChangeEvent, useContext, useState } from 'react';
|
||||
import { IObjectMeta } from '@kubernetes-models/apimachinery/apis/meta/v1/ObjectMeta';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
makeStyles,
|
||||
IconButton,
|
||||
createStyles,
|
||||
Theme,
|
||||
Drawer,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Grid,
|
||||
IconButton,
|
||||
Switch,
|
||||
Typography,
|
||||
Button,
|
||||
withStyles,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import { V1ObjectMeta } from '@kubernetes/client-node';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import jsYaml from 'js-yaml';
|
||||
import {
|
||||
LinkButton as BackstageButton,
|
||||
CodeSnippet,
|
||||
StructuredMetadataTable,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { ClusterContext } from '../../hooks';
|
||||
import { formatClusterLink } from '../../utils/clusterLinks';
|
||||
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
|
||||
import { FormatClusterLinkOptions } from '../../utils/clusterLinks/formatClusterLink';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { ManifestYaml } from './ManifestYaml';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface KubernetesObject {
|
||||
kind: string;
|
||||
metadata?: IObjectMeta;
|
||||
}
|
||||
|
||||
interface KubernetesDrawerContentProps {
|
||||
close: () => void;
|
||||
kubernetesObject: KubernetesObject;
|
||||
header?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesDrawerContent = ({
|
||||
children,
|
||||
header,
|
||||
kubernetesObject,
|
||||
close,
|
||||
}: KubernetesDrawerContentProps) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
const [isYaml, setIsYaml] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Grid container justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={11}>
|
||||
<Typography variant="h5">
|
||||
{kubernetesObject.metadata?.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={() => close()}
|
||||
color="inherit"
|
||||
>
|
||||
<CloseIcon className={classes.icon} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{header}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml}
|
||||
onChange={event => {
|
||||
setIsYaml(event.target.checked);
|
||||
}}
|
||||
name="YAML"
|
||||
/>
|
||||
}
|
||||
label="YAML"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && <ManifestYaml object={kubernetesObject} />}
|
||||
{!isYaml && children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface KubernetesDrawerProps {
|
||||
open?: boolean;
|
||||
kubernetesObject: KubernetesObject;
|
||||
label: React.ReactNode;
|
||||
drawerContentsHeader?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -54,32 +133,7 @@ const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
}),
|
||||
);
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
errorMessage: {
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
},
|
||||
options: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const PodDrawerButton = withStyles({
|
||||
const DrawerButton = withStyles({
|
||||
root: {
|
||||
padding: '6px 5px',
|
||||
},
|
||||
@@ -88,203 +142,15 @@ const PodDrawerButton = withStyles({
|
||||
},
|
||||
})(Button);
|
||||
|
||||
type ErrorPanelProps = {
|
||||
cluster: ClusterAttributes;
|
||||
errorMessage?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => (
|
||||
<WarningPanel
|
||||
title="There was a problem formatting the link to the Kubernetes dashboard"
|
||||
message={`Could not format the link to the dashboard of your cluster named '${
|
||||
cluster.name
|
||||
}'. Its dashboardApp property has been set to '${
|
||||
cluster.dashboardApp || 'standard'
|
||||
}.'`}
|
||||
>
|
||||
{errorMessage && (
|
||||
<Typography variant="body2">Errors: {errorMessage}</Typography>
|
||||
)}
|
||||
</WarningPanel>
|
||||
);
|
||||
|
||||
interface KubernetesDrawerable {
|
||||
metadata?: V1ObjectMeta;
|
||||
}
|
||||
|
||||
interface KubernetesDrawerContentProps<T extends KubernetesDrawerable> {
|
||||
toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void;
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
function replaceNullsWithUndefined(someObj: any) {
|
||||
const replacer = (_: any, value: any) =>
|
||||
String(value) === 'null' || String(value) === 'undefined'
|
||||
? undefined
|
||||
: value;
|
||||
|
||||
return JSON.parse(JSON.stringify(someObj, replacer));
|
||||
}
|
||||
|
||||
function tryFormatClusterLink(options: FormatClusterLinkOptions) {
|
||||
try {
|
||||
return {
|
||||
clusterLink: formatClusterLink(options),
|
||||
errorMessage: '',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
clusterLink: '',
|
||||
errorMessage: err.message || err.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const KubernetesDrawerContent = <T extends KubernetesDrawerable>({
|
||||
toggleDrawer,
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
}: KubernetesDrawerContentProps<T>) => {
|
||||
const [isYaml, setIsYaml] = useState<boolean>(false);
|
||||
|
||||
// Toggle whether the Kubernetes resource managed fields should be shown in
|
||||
// the YAML display. This toggle is only available when the YAML is being
|
||||
// shown because managed fields are never visible in the structured display.
|
||||
const [managedFields, setManagedFields] = useState<boolean>(false);
|
||||
|
||||
const classes = useDrawerContentStyles();
|
||||
const cluster = useContext(ClusterContext);
|
||||
const { clusterLink, errorMessage } = tryFormatClusterLink({
|
||||
dashboardUrl: cluster.dashboardUrl,
|
||||
dashboardApp: cluster.dashboardApp,
|
||||
dashboardParameters: cluster.dashboardParameters,
|
||||
object,
|
||||
kind,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Grid
|
||||
container
|
||||
direction="column"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant="h5">
|
||||
{object.metadata?.name ?? 'unknown name'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
{kind}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={e => toggleDrawer(e, false)}
|
||||
color="inherit"
|
||||
>
|
||||
<Close className={classes.icon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className={classes.errorMessage}>
|
||||
<ErrorPanel cluster={cluster} errorMessage={errorMessage} />
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.options}>
|
||||
<div>
|
||||
{clusterLink && (
|
||||
<BackstageButton
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
to={clusterLink}
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
Open Kubernetes Dashboard
|
||||
</BackstageButton>
|
||||
)}
|
||||
</div>
|
||||
<FormGroup className={classes.options}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml}
|
||||
onChange={event => {
|
||||
setIsYaml(event.target.checked);
|
||||
}}
|
||||
name="YAML"
|
||||
/>
|
||||
}
|
||||
label="YAML"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml && managedFields}
|
||||
onChange={event => {
|
||||
if (isYaml) {
|
||||
setManagedFields(event.target.checked);
|
||||
}
|
||||
}}
|
||||
name="Managed Fields"
|
||||
/>
|
||||
}
|
||||
label="Managed Fields"
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && (
|
||||
<CodeSnippet
|
||||
language="yaml"
|
||||
text={jsYaml.dump(object, {
|
||||
replacer: (key: string, value: string): any => {
|
||||
if (!managedFields) {
|
||||
return key === 'managedFields' ? undefined : value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{!isYaml && (
|
||||
<StructuredMetadataTable
|
||||
metadata={renderObject(replaceNullsWithUndefined(object))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
interface KubernetesDrawerProps<T extends KubernetesDrawerable> {
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
buttonVariant?: 'h5' | 'subtitle2';
|
||||
kind: string;
|
||||
expanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesDrawer = <T extends KubernetesDrawerable>({
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
buttonVariant = 'subtitle2',
|
||||
expanded = false,
|
||||
export const KubernetesDrawer = ({
|
||||
open,
|
||||
label,
|
||||
drawerContentsHeader,
|
||||
kubernetesObject,
|
||||
children,
|
||||
}: KubernetesDrawerProps<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(expanded);
|
||||
}: KubernetesDrawerProps) => {
|
||||
const classes = useDrawerStyles();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(open ?? false);
|
||||
|
||||
const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => {
|
||||
e.stopPropagation();
|
||||
@@ -293,18 +159,7 @@ export const KubernetesDrawer = <T extends KubernetesDrawerable>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<PodDrawerButton
|
||||
onClick={e => toggleDrawer(e, true)}
|
||||
onFocus={event => event.stopPropagation()}
|
||||
>
|
||||
{children === undefined ? (
|
||||
<Typography variant={buttonVariant}>
|
||||
{object.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</PodDrawerButton>
|
||||
<DrawerButton onClick={() => setIsOpen(true)}>{label}</DrawerButton>
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
@@ -314,12 +169,14 @@ export const KubernetesDrawer = <T extends KubernetesDrawerable>({
|
||||
onClose={(e: any) => toggleDrawer(e, false)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<KubernetesDrawerContent
|
||||
kind={kind}
|
||||
toggleDrawer={toggleDrawer}
|
||||
object={object}
|
||||
renderObject={renderObject}
|
||||
/>
|
||||
{isOpen && (
|
||||
<KubernetesDrawerContent
|
||||
header={drawerContentsHeader}
|
||||
kubernetesObject={kubernetesObject}
|
||||
children={children}
|
||||
close={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright 2020 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, { ChangeEvent, useContext, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
makeStyles,
|
||||
IconButton,
|
||||
createStyles,
|
||||
Theme,
|
||||
Drawer,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
} from '@material-ui/core';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import { V1ObjectMeta } from '@kubernetes/client-node';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
LinkButton as BackstageButton,
|
||||
StructuredMetadataTable,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { ClusterContext } from '../../hooks';
|
||||
import { formatClusterLink } from '../../utils/clusterLinks';
|
||||
import { ClusterAttributes } from '@backstage/plugin-kubernetes-common';
|
||||
import { FormatClusterLinkOptions } from '../../utils/clusterLinks/formatClusterLink';
|
||||
import { ManifestYaml } from './ManifestYaml';
|
||||
|
||||
const useDrawerStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paper: {
|
||||
width: '50%',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2.5),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
errorMessage: {
|
||||
marginTop: '1em',
|
||||
marginBottom: '1em',
|
||||
},
|
||||
options: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const PodDrawerButton = withStyles({
|
||||
root: {
|
||||
padding: '6px 5px',
|
||||
},
|
||||
label: {
|
||||
textTransform: 'none',
|
||||
},
|
||||
})(Button);
|
||||
|
||||
type ErrorPanelProps = {
|
||||
cluster: ClusterAttributes;
|
||||
errorMessage?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LinkErrorPanel = ({ cluster, errorMessage }: ErrorPanelProps) => (
|
||||
<WarningPanel
|
||||
title="There was a problem formatting the link to the Kubernetes dashboard"
|
||||
message={`Could not format the link to the dashboard of your cluster named '${
|
||||
cluster.name
|
||||
}'. Its dashboardApp property has been set to '${
|
||||
cluster.dashboardApp || 'standard'
|
||||
}.'`}
|
||||
>
|
||||
{errorMessage && (
|
||||
<Typography variant="body2">Errors: {errorMessage}</Typography>
|
||||
)}
|
||||
</WarningPanel>
|
||||
);
|
||||
|
||||
interface KubernetesDrawerable {
|
||||
metadata?: V1ObjectMeta;
|
||||
}
|
||||
|
||||
interface KubernetesStructuredMetadataTableDrawerContentProps<
|
||||
T extends KubernetesDrawerable,
|
||||
> {
|
||||
toggleDrawer: (e: ChangeEvent<{}>, isOpen: boolean) => void;
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
function replaceNullsWithUndefined(someObj: any) {
|
||||
const replacer = (_: any, value: any) =>
|
||||
String(value) === 'null' || String(value) === 'undefined'
|
||||
? undefined
|
||||
: value;
|
||||
|
||||
return JSON.parse(JSON.stringify(someObj, replacer));
|
||||
}
|
||||
|
||||
function tryFormatClusterLink(options: FormatClusterLinkOptions) {
|
||||
try {
|
||||
return {
|
||||
clusterLink: formatClusterLink(options),
|
||||
errorMessage: '',
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
clusterLink: '',
|
||||
errorMessage: err.message || err.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const KubernetesStructuredMetadataTableDrawerContent = <
|
||||
T extends KubernetesDrawerable,
|
||||
>({
|
||||
toggleDrawer,
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
}: KubernetesStructuredMetadataTableDrawerContentProps<T>) => {
|
||||
const [isYaml, setIsYaml] = useState<boolean>(false);
|
||||
|
||||
const classes = useDrawerContentStyles();
|
||||
const cluster = useContext(ClusterContext);
|
||||
const { clusterLink, errorMessage } = tryFormatClusterLink({
|
||||
dashboardUrl: cluster.dashboardUrl,
|
||||
dashboardApp: cluster.dashboardApp,
|
||||
dashboardParameters: cluster.dashboardParameters,
|
||||
object,
|
||||
kind,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.header}>
|
||||
<Grid container justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={11}>
|
||||
<Typography variant="h5">
|
||||
{object.metadata?.name ?? 'unknown name'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<IconButton
|
||||
key="dismiss"
|
||||
title="Close the drawer"
|
||||
onClick={e => toggleDrawer(e, false)}
|
||||
color="inherit"
|
||||
>
|
||||
<Close className={classes.icon} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={11}>
|
||||
<Typography color="textSecondary" variant="body1">
|
||||
{kind}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={11}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isYaml}
|
||||
onChange={event => {
|
||||
setIsYaml(event.target.checked);
|
||||
}}
|
||||
name="YAML"
|
||||
/>
|
||||
}
|
||||
label="YAML"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<div className={classes.errorMessage}>
|
||||
<LinkErrorPanel cluster={cluster} errorMessage={errorMessage} />
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.options}>
|
||||
<div>
|
||||
{clusterLink && (
|
||||
<BackstageButton
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
size="small"
|
||||
to={clusterLink}
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
Open Kubernetes Dashboard
|
||||
</BackstageButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.content}>
|
||||
{isYaml && <ManifestYaml object={object} />}
|
||||
{!isYaml && (
|
||||
<StructuredMetadataTable
|
||||
metadata={renderObject(replaceNullsWithUndefined(object))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
interface KubernetesStructuredMetadataTableDrawerProps<
|
||||
T extends KubernetesDrawerable,
|
||||
> {
|
||||
object: T;
|
||||
renderObject: (obj: T) => object;
|
||||
buttonVariant?: 'h5' | 'subtitle2';
|
||||
kind: string;
|
||||
expanded?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const KubernetesStructuredMetadataTableDrawer = <
|
||||
T extends KubernetesDrawerable,
|
||||
>({
|
||||
object,
|
||||
renderObject,
|
||||
kind,
|
||||
buttonVariant = 'subtitle2',
|
||||
expanded = false,
|
||||
children,
|
||||
}: KubernetesStructuredMetadataTableDrawerProps<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(expanded);
|
||||
const classes = useDrawerStyles();
|
||||
|
||||
const toggleDrawer = (e: ChangeEvent<{}>, newValue: boolean) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PodDrawerButton
|
||||
onClick={e => toggleDrawer(e, true)}
|
||||
onFocus={event => event.stopPropagation()}
|
||||
>
|
||||
{children === undefined ? (
|
||||
<Typography variant={buttonVariant}>
|
||||
{object.metadata?.name ?? 'unknown object'}
|
||||
</Typography>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</PodDrawerButton>
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={(e: any) => toggleDrawer(e, false)}
|
||||
onClick={event => event.stopPropagation()}
|
||||
>
|
||||
<KubernetesStructuredMetadataTableDrawerContent
|
||||
kind={kind}
|
||||
toggleDrawer={toggleDrawer}
|
||||
object={object}
|
||||
renderObject={renderObject}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { CodeSnippet } from '@backstage/core-components';
|
||||
import { FormControlLabel, Switch } from '@material-ui/core';
|
||||
import jsyaml from 'js-yaml';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface ManifestYamlProps {
|
||||
object: object;
|
||||
}
|
||||
|
||||
export const ManifestYaml = ({ object }: ManifestYamlProps) => {
|
||||
// Toggle whether the Kubernetes resource managed fields should be shown in
|
||||
// the YAML display. This toggle is only available when the YAML is being
|
||||
// shown because managed fields are never visible in the structured display.
|
||||
const [managedFields, setManagedFields] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={managedFields}
|
||||
onChange={event => {
|
||||
setManagedFields(event.target.checked);
|
||||
}}
|
||||
name="Managed Fields"
|
||||
/>
|
||||
}
|
||||
label="Managed Fields"
|
||||
/>
|
||||
<CodeSnippet
|
||||
language="yaml"
|
||||
text={jsyaml.dump(object, {
|
||||
// NOTE: this will remove any field called `managedFields`
|
||||
// not just the metadata one
|
||||
// TODO: @mclarke make this only remove the `metadata.managedFields`
|
||||
replacer: (key: string, value: string): any => {
|
||||
if (!managedFields) {
|
||||
return key === 'managedFields' ? undefined : value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './KubernetesStructuredMetadataTableDrawer';
|
||||
export * from './KubernetesDrawer';
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 * as pod from './__fixtures__/pod.json';
|
||||
import * as crashingPod from './__fixtures__/crashing-pod.json';
|
||||
import { textContentMatcher, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { PodDrawer } from './PodDrawer';
|
||||
|
||||
describe('PodDrawer', () => {
|
||||
it('should render pod', async () => {
|
||||
const { getByText, getAllByText } = render(
|
||||
wrapInTestApp(<PodDrawer pod={pod as any} expanded />),
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller-6c8646bfd-2m5hv')).toHaveLength(2);
|
||||
expect(getByText('Pod')).toBeInTheDocument();
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Images')).toBeInTheDocument();
|
||||
expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument();
|
||||
expect(getByText('Phase')).toBeInTheDocument();
|
||||
expect(getByText('Running')).toBeInTheDocument();
|
||||
expect(getAllByText('Containers Ready')).toHaveLength(2);
|
||||
expect(getByText('1/1')).toBeInTheDocument();
|
||||
expect(getByText('Total Restarts')).toBeInTheDocument();
|
||||
expect(getByText('0')).toBeInTheDocument();
|
||||
expect(getByText('Container Statuses')).toBeInTheDocument();
|
||||
expect(getByText('OK')).toBeInTheDocument();
|
||||
expect(getByText('Initialized')).toBeInTheDocument();
|
||||
expect(getByText('Ready')).toBeInTheDocument();
|
||||
expect(getByText('Pod Scheduled')).toBeInTheDocument();
|
||||
expect(getAllByText('True')).toHaveLength(4);
|
||||
expect(getByText('Exposed Ports')).toBeInTheDocument();
|
||||
expect(getByText('Nginx:')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Container Port: 80')),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText(textContentMatcher('Protocol: TCP'))).toBeInTheDocument();
|
||||
});
|
||||
it('should render crashing pod', async () => {
|
||||
const { getByText, getAllByText } = render(
|
||||
wrapInTestApp(<PodDrawer pod={crashingPod as any} expanded />),
|
||||
);
|
||||
|
||||
expect(getAllByText('dice-roller-canary-7d64cd756c-55rfq')).toHaveLength(2);
|
||||
expect(getByText('Pod')).toBeInTheDocument();
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Images')).toBeInTheDocument();
|
||||
expect(getByText('nginx=nginx:1.14.2')).toBeInTheDocument();
|
||||
expect(getByText('other-side-car=nginx:1.14.2')).toBeInTheDocument();
|
||||
expect(getByText('side-car=nginx:1.14.2')).toBeInTheDocument();
|
||||
expect(getByText('Phase')).toBeInTheDocument();
|
||||
expect(getByText('Running')).toBeInTheDocument();
|
||||
expect(getAllByText('Containers Ready')).toHaveLength(2);
|
||||
expect(getByText('1/3')).toBeInTheDocument();
|
||||
expect(getByText('Total Restarts')).toBeInTheDocument();
|
||||
expect(getByText('76')).toBeInTheDocument();
|
||||
expect(getByText('Container Statuses')).toBeInTheDocument();
|
||||
expect(getByText('Container: side-car')).toBeInTheDocument();
|
||||
expect(getByText('Container: other-side-car')).toBeInTheDocument();
|
||||
expect(getAllByText('CrashLoopBackOff')).toHaveLength(2);
|
||||
expect(getByText('Initialized')).toBeInTheDocument();
|
||||
expect(getByText('Ready')).toBeInTheDocument();
|
||||
expect(getByText('Pod Scheduled')).toBeInTheDocument();
|
||||
expect(getAllByText('True')).toHaveLength(2);
|
||||
expect(getAllByText('False')).toHaveLength(2);
|
||||
expect(
|
||||
getAllByText('containers with unready status: [side-car other-side-car]'),
|
||||
).toHaveLength(2);
|
||||
expect(getByText('Exposed Ports')).toBeInTheDocument();
|
||||
expect(getAllByText(textContentMatcher('Protocol: TCP'))).toHaveLength(3);
|
||||
expect(getByText('Nginx:')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Container Port: 80')),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Side Car:')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Container Port: 81')),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Other Side Car:')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(textContentMatcher('Container Port: 82')),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { V1Pod } from '@kubernetes/client-node';
|
||||
import {
|
||||
containersReady,
|
||||
containerStatuses,
|
||||
totalRestarts,
|
||||
imageChips,
|
||||
renderCondition,
|
||||
} from '../../utils/pod';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
|
||||
/** @public */
|
||||
export const PodDrawer = (props: { pod: V1Pod; expanded?: boolean }) => {
|
||||
const { pod, expanded } = props;
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
object={pod}
|
||||
expanded={expanded}
|
||||
kind="Pod"
|
||||
renderObject={(podObject: V1Pod) => {
|
||||
const phase = podObject.status?.phase ?? 'unknown';
|
||||
|
||||
const ports =
|
||||
podObject.spec?.containers?.map(c => {
|
||||
return {
|
||||
[c.name]: c.ports,
|
||||
};
|
||||
}) ?? 'N/A';
|
||||
|
||||
const conditions = (podObject.status?.conditions ?? [])
|
||||
.map(renderCondition)
|
||||
.reduce((accum, next) => {
|
||||
accum[next[0]] = next[1];
|
||||
return accum;
|
||||
}, {} as { [key: string]: React.ReactNode });
|
||||
|
||||
return {
|
||||
images: imageChips(podObject),
|
||||
phase: phase,
|
||||
'Containers Ready': containersReady(podObject),
|
||||
'Total Restarts': totalRestarts(podObject),
|
||||
'Container Statuses': containerStatuses(podObject),
|
||||
...conditions,
|
||||
'Exposed ports': ports,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ContainerCard } from './ContainerCard';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const now = DateTime.now();
|
||||
const oneHourAgo = now.minus({ hours: 1 }).toISO();
|
||||
const twoHoursAgo = now.minus({ hours: 2 }).toISO();
|
||||
|
||||
describe('ContainerCard', () => {
|
||||
it('show healthy when all checks pass', async () => {
|
||||
const { getByText, queryByText, getAllByText } = render(
|
||||
wrapInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
name: 'some-name',
|
||||
namespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {
|
||||
readinessProbe: {},
|
||||
},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: true,
|
||||
ready: true,
|
||||
restartCount: 0,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: oneHourAgo,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Started: 1 hour ago')).toBeInTheDocument();
|
||||
expect(getByText('Status: Running')).toBeInTheDocument();
|
||||
expect(getByText('some-name')).toBeInTheDocument();
|
||||
expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(getAllByText('✅')).toHaveLength(5);
|
||||
expect(queryByText('❌')).toBeNull();
|
||||
});
|
||||
it('show unhealthy when all checks fail', async () => {
|
||||
const { getByText, queryByText, getAllByText } = render(
|
||||
wrapInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: false,
|
||||
ready: false,
|
||||
restartCount: 12,
|
||||
state: {
|
||||
waiting: {},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('some-name')).toBeInTheDocument();
|
||||
expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(getAllByText('❌')).toHaveLength(5);
|
||||
expect(queryByText('✅')).toBeNull();
|
||||
});
|
||||
it('show correct checks for completed container', async () => {
|
||||
const { getByText, queryByText, getAllByText } = render(
|
||||
wrapInTestApp(
|
||||
<ContainerCard
|
||||
{...({
|
||||
podScope: {
|
||||
podName: 'some-name',
|
||||
podNamespace: 'some-namespace',
|
||||
clusterName: 'some-cluster',
|
||||
},
|
||||
containerSpec: {},
|
||||
containerStatus: {
|
||||
name: 'some-name',
|
||||
image: 'gcr.io/some-proj/some-image',
|
||||
started: false,
|
||||
ready: false,
|
||||
restartCount: 0,
|
||||
state: {
|
||||
terminated: {
|
||||
exitCode: 0,
|
||||
reason: 'Completed',
|
||||
startedAt: twoHoursAgo,
|
||||
finishedAt: oneHourAgo,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('some-name')).toBeInTheDocument();
|
||||
expect(getByText('gcr.io/some-proj/some-image')).toBeInTheDocument();
|
||||
expect(getByText('Started: 2 hours ago')).toBeInTheDocument();
|
||||
expect(getByText('Completed: 1 hour ago')).toBeInTheDocument();
|
||||
expect(
|
||||
getByText('Execution time: 1 hour, 0 minutes, 0 seconds'),
|
||||
).toBeInTheDocument();
|
||||
expect(getByText('Status: Completed')).toBeInTheDocument();
|
||||
expect(getAllByText('✅')).toHaveLength(2);
|
||||
expect(queryByText('❌')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 { IContainer, IContainerStatus } from 'kubernetes-models/v1';
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { PodScope, PodLogsDialog } from '../PodLogs';
|
||||
import { StructuredMetadataTable } from '@backstage/core-components';
|
||||
|
||||
const getContainerHealthChecks = (
|
||||
containerSpec: IContainer,
|
||||
containerStatus: IContainerStatus,
|
||||
): { [key: string]: boolean } => {
|
||||
if (containerStatus.state?.terminated?.reason === 'Completed') {
|
||||
return {
|
||||
'not waiting to start': containerStatus.state?.waiting === undefined,
|
||||
'no restarts': containerStatus.restartCount === 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
'not waiting to start': containerStatus.state?.waiting === undefined,
|
||||
started: !!containerStatus.started,
|
||||
ready: containerStatus.ready,
|
||||
'no restarts': containerStatus.restartCount === 0,
|
||||
'readiness probe set':
|
||||
containerSpec && containerSpec?.readinessProbe !== undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getCurrentState = (containerStatus: IContainerStatus): string => {
|
||||
return (
|
||||
containerStatus.state?.waiting?.reason ||
|
||||
containerStatus.state?.terminated?.reason ||
|
||||
(containerStatus.state?.running !== undefined ? 'Running' : 'Unknown')
|
||||
);
|
||||
};
|
||||
|
||||
const getStartedAtTime = (
|
||||
containerStatus: IContainerStatus,
|
||||
): string | undefined => {
|
||||
return (
|
||||
containerStatus.state?.running?.startedAt ||
|
||||
containerStatus.state?.terminated?.startedAt
|
||||
);
|
||||
};
|
||||
|
||||
interface ContainerDatetimeProps {
|
||||
prefix: string;
|
||||
dateTime: string;
|
||||
}
|
||||
|
||||
const ContainerDatetime = ({ prefix, dateTime }: ContainerDatetimeProps) => {
|
||||
return (
|
||||
<Typography variant="subtitle2">
|
||||
{prefix}:{' '}
|
||||
{DateTime.fromISO(dateTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ContainerCardProps {
|
||||
podScope: PodScope;
|
||||
containerSpec?: IContainer;
|
||||
containerStatus: IContainerStatus;
|
||||
}
|
||||
|
||||
export const ContainerCard: React.FC<ContainerCardProps> = ({
|
||||
podScope,
|
||||
containerSpec,
|
||||
containerStatus,
|
||||
}: ContainerCardProps) => {
|
||||
// This should never be undefined
|
||||
if (containerSpec === undefined) {
|
||||
return <Typography>error reading pod from cluster</Typography>;
|
||||
}
|
||||
const containerStartedTime = getStartedAtTime(containerStatus);
|
||||
const containerFinishedTime = containerStatus.state?.terminated?.finishedAt;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={containerStatus.name}
|
||||
subheader={containerStatus.image}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{containerStartedTime && (
|
||||
<ContainerDatetime
|
||||
prefix="Started"
|
||||
dateTime={containerStartedTime}
|
||||
/>
|
||||
)}
|
||||
{containerFinishedTime && (
|
||||
<ContainerDatetime
|
||||
prefix="Completed"
|
||||
dateTime={containerFinishedTime}
|
||||
/>
|
||||
)}
|
||||
{containerStartedTime && containerFinishedTime && (
|
||||
<Typography variant="subtitle2">
|
||||
Execution time:{' '}
|
||||
{DateTime.fromISO(containerFinishedTime)
|
||||
.diff(DateTime.fromISO(containerStartedTime), [
|
||||
'hours',
|
||||
'minutes',
|
||||
'seconds',
|
||||
])
|
||||
.toHuman()}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">
|
||||
Status: {getCurrentState(containerStatus)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{containerStatus.restartCount > 0 && (
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">
|
||||
Restarts: {containerStatus.restartCount}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle2">Container health</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<StructuredMetadataTable
|
||||
metadata={getContainerHealthChecks(
|
||||
containerSpec,
|
||||
containerStatus,
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<PodLogsDialog
|
||||
podScope={{
|
||||
containerName: containerStatus.name,
|
||||
...podScope,
|
||||
}}
|
||||
/>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { PendingPodContent } from './PendingPodContent';
|
||||
import { IPodCondition } from 'kubernetes-models/v1';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const podWithConditions = (conditions: IPodCondition[]): any => {
|
||||
return {
|
||||
metadata: {
|
||||
name: 'ok-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
conditions: conditions,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('PendingPodContent', () => {
|
||||
it('show startup conditions - all healthy', async () => {
|
||||
const oneDayAgo = DateTime.now().minus({ days: 1 }).toISO();
|
||||
const { getByText, queryByLabelText, queryAllByLabelText } = render(
|
||||
wrapInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'PodScheduled',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'ContainersReady',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
{
|
||||
type: 'Ready',
|
||||
status: 'True',
|
||||
lastTransitionTime: oneDayAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(getByText('Initialized - (1 day ago)')).toBeInTheDocument();
|
||||
expect(getByText('PodScheduled - (1 day ago)')).toBeInTheDocument();
|
||||
expect(getByText('ContainersReady - (1 day ago)')).toBeInTheDocument();
|
||||
expect(getByText('Ready - (1 day ago)')).toBeInTheDocument();
|
||||
|
||||
expect(queryAllByLabelText('Status ok')).toHaveLength(4);
|
||||
expect(queryByLabelText('Status warning')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText('Status error')).not.toBeInTheDocument();
|
||||
});
|
||||
it('show startup conditions - all fail', async () => {
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
|
||||
const { getByText, queryByLabelText, queryAllByLabelText } = render(
|
||||
wrapInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'False',
|
||||
reason: 'InitializedFailureReason',
|
||||
message: 'reason why Initialized failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'PodScheduled',
|
||||
status: 'False',
|
||||
reason: 'PodScheduledFailureReason',
|
||||
message: 'reason why PodScheduled failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'ContainersReady',
|
||||
status: 'False',
|
||||
reason: 'ContainersReadyFailureReason',
|
||||
message: 'reason why ContainersReady failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
{
|
||||
type: 'Ready',
|
||||
status: 'False',
|
||||
reason: 'ReadyFailureReason',
|
||||
message: 'reason why Ready failed',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
getByText(
|
||||
'Initialized - (InitializedFailureReason 1 hour ago) - reason why Initialized failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'PodScheduled - (PodScheduledFailureReason 1 hour ago) - reason why PodScheduled failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'ContainersReady - (ContainersReadyFailureReason 1 hour ago) - reason why ContainersReady failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByText(
|
||||
'Ready - (ReadyFailureReason 1 hour ago) - reason why Ready failed',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(queryByLabelText('Status ok')).not.toBeInTheDocument();
|
||||
expect(queryByLabelText('Status warning')).not.toBeInTheDocument();
|
||||
expect(queryAllByLabelText('Status error')).toHaveLength(4);
|
||||
});
|
||||
it('show startup conditions - show unknown', async () => {
|
||||
const oneHourAgo = DateTime.now().minus({ hours: 1 }).toISO();
|
||||
const { getByText, queryByLabelText, getByLabelText } = render(
|
||||
wrapInTestApp(
|
||||
<PendingPodContent
|
||||
{...{
|
||||
pod: podWithConditions([
|
||||
{
|
||||
type: 'Initialized',
|
||||
status: 'Unknown',
|
||||
reason: 'InitializedUnknownReason',
|
||||
message: 'dont know what is happening',
|
||||
lastTransitionTime: oneHourAgo,
|
||||
},
|
||||
]),
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
expect(getByText('Pod is Pending. Conditions:')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
getByText('Initialized - (1 hour ago) dont know what is happening'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(queryByLabelText('Status ok')).not.toBeInTheDocument();
|
||||
expect(getByLabelText('Status warning')).toBeInTheDocument();
|
||||
expect(queryByLabelText('Status error')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 { Grid, List, ListItem, Typography } from '@material-ui/core';
|
||||
import { IPodCondition, Pod } from 'kubernetes-models/v1';
|
||||
import {
|
||||
StatusError,
|
||||
StatusOK,
|
||||
StatusWarning,
|
||||
} from '@backstage/core-components';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface PodConditionProps {
|
||||
condition: IPodCondition;
|
||||
}
|
||||
|
||||
export const PodCondition = ({ condition }: PodConditionProps) => {
|
||||
return (
|
||||
<>
|
||||
{condition.status === 'False' && (
|
||||
<StatusError>
|
||||
{condition.type} - ({condition.reason}{' '}
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
) - {condition.message}{' '}
|
||||
</StatusError>
|
||||
)}
|
||||
{condition.status === 'True' && (
|
||||
<StatusOK>
|
||||
{condition.type} - (
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
)
|
||||
</StatusOK>
|
||||
)}
|
||||
{condition.status === 'Unknown' && (
|
||||
<StatusWarning>
|
||||
{condition.type} - (
|
||||
{condition.lastTransitionTime &&
|
||||
DateTime.fromISO(condition.lastTransitionTime).toRelative({
|
||||
locale: 'en',
|
||||
})}
|
||||
) {condition.message}
|
||||
</StatusWarning>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface PendingPodContentProps {
|
||||
pod: Pod;
|
||||
}
|
||||
|
||||
export const PendingPodContent = ({ pod }: PendingPodContentProps) => {
|
||||
// TODO add PodHasNetwork when it's out of alpha
|
||||
// https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions
|
||||
const startupConditions = [
|
||||
pod.status?.conditions?.find(c => c.type === 'PodScheduled'),
|
||||
pod.status?.conditions?.find(c => c.type === 'Initialized'),
|
||||
pod.status?.conditions?.find(c => c.type === 'ContainersReady'),
|
||||
pod.status?.conditions?.find(c => c.type === 'Ready'),
|
||||
].filter((c): c is IPodCondition => !!c); // filter out undefined
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Pod is Pending. Conditions:</Typography>
|
||||
<List>
|
||||
{startupConditions.map(c => (
|
||||
<ListItem key={c.type}>
|
||||
<PodCondition condition={c} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2020 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 { wrapInTestApp } from '@backstage/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { PodDrawer } from '.';
|
||||
|
||||
describe('PodDrawer', () => {
|
||||
it('Should show title and container names', async () => {
|
||||
const { getAllByText, getByText } = render(
|
||||
wrapInTestApp(
|
||||
<PodDrawer
|
||||
{...({
|
||||
open: true,
|
||||
podAndErrors: {
|
||||
clusterName: 'some-cluster-1',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'ok-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(getAllByText('ok-pod')).toHaveLength(2);
|
||||
expect(getByText('Pod (127.0.0.1)')).toBeInTheDocument();
|
||||
expect(getByText('YAML')).toBeInTheDocument();
|
||||
expect(getByText('Containers')).toBeInTheDocument();
|
||||
expect(getByText('some-container')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ItemCardGrid } from '@backstage/core-components';
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { Pod } from 'kubernetes-models/v1';
|
||||
|
||||
import { ContainerCard } from './ContainerCard';
|
||||
|
||||
import { PodAndErrors } from '../types';
|
||||
import { KubernetesDrawer } from '../../KubernetesDrawer';
|
||||
import { PendingPodContent } from './PendingPodContent';
|
||||
|
||||
const useDrawerContentStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
content: {
|
||||
height: '80%',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
},
|
||||
podoklist: {
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
maxHeight: 360,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function getContainerSpecByName(pod: Pod, containerName: string) {
|
||||
return pod.spec?.containers.find(c => c.name === containerName);
|
||||
}
|
||||
interface PodDrawerProps {
|
||||
open?: boolean;
|
||||
podAndErrors: PodAndErrors;
|
||||
}
|
||||
|
||||
export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
|
||||
const classes = useDrawerContentStyles();
|
||||
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
open={open}
|
||||
drawerContentsHeader={
|
||||
<Typography variant="subtitle1">
|
||||
Pod{' '}
|
||||
{podAndErrors.pod.status?.podIP &&
|
||||
`(${podAndErrors.pod.status?.podIP})`}
|
||||
</Typography>
|
||||
}
|
||||
kubernetesObject={podAndErrors.pod}
|
||||
label={
|
||||
<Typography variant="subtitle1">
|
||||
{podAndErrors.pod.metadata?.name ?? 'unknown'}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
{podAndErrors.pod.status?.phase === 'Pending' && (
|
||||
<PendingPodContent pod={podAndErrors.pod} />
|
||||
)}
|
||||
{podAndErrors.pod.status?.containerStatuses?.length && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h5">Containers</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<ItemCardGrid>
|
||||
{podAndErrors.pod.status?.containerStatuses?.map(
|
||||
(containerStatus, i) => {
|
||||
const containerSpec = getContainerSpecByName(
|
||||
podAndErrors.pod,
|
||||
containerStatus.name,
|
||||
);
|
||||
return (
|
||||
<ContainerCard
|
||||
key={`container-card-${podAndErrors.pod.metadata?.name}-${i}`}
|
||||
podScope={{
|
||||
podName: podAndErrors.pod.metadata?.name ?? 'unknown',
|
||||
podNamespace:
|
||||
podAndErrors.pod.metadata?.namespace ?? 'unknown',
|
||||
clusterName: podAndErrors.clusterName,
|
||||
}}
|
||||
containerSpec={containerSpec}
|
||||
containerStatus={containerStatus}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ItemCardGrid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
</KubernetesDrawer>
|
||||
);
|
||||
};
|
||||
@@ -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 { PodDrawer } from './PodDrawer';
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { DismissableBanner, LogViewer } from '@backstage/core-components';
|
||||
import { Paper } from '@material-ui/core';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
|
||||
import { ContainerScope } from './types';
|
||||
import { usePodLogs } from './usePodLogs';
|
||||
|
||||
interface PodLogsProps {
|
||||
podScope: ContainerScope;
|
||||
}
|
||||
|
||||
export const PodLogs: React.FC<PodLogsProps> = ({ podScope }: PodLogsProps) => {
|
||||
const { value, error, loading } = usePodLogs({
|
||||
podScope: podScope,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<DismissableBanner
|
||||
{...{
|
||||
message: error.message,
|
||||
variant: 'error',
|
||||
fixed: false,
|
||||
}}
|
||||
id="pod-logs"
|
||||
/>
|
||||
)}
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{ height: '100%', width: '100%', minHeight: '30rem' }}
|
||||
>
|
||||
{loading && <Skeleton variant="rect" width="100%" height="100%" />}
|
||||
{!loading && value !== undefined && <LogViewer text={value.text} />}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import SubjectIcon from '@material-ui/icons/Subject';
|
||||
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
import { PodLogs } from './PodLogs';
|
||||
import { ContainerScope } from './types';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface PodLogsDialogProps {
|
||||
podScope: ContainerScope;
|
||||
}
|
||||
|
||||
export const PodLogsDialog = ({ podScope }: PodLogsDialogProps) => {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const openDialog = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Dialog maxWidth="xl" fullWidth open={open} onClose={closeDialog}>
|
||||
<DialogTitle id="dialog-title">
|
||||
{podScope.podName} - {podScope.containerName} logs on cluster{' '}
|
||||
{podScope.clusterName}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<PodLogs podScope={podScope} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="get logs"
|
||||
component="label"
|
||||
onClick={openDialog}
|
||||
startIcon={<SubjectIcon />}
|
||||
>
|
||||
Logs
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 './PodLogs';
|
||||
export * from './PodLogsDialog';
|
||||
export * from './usePodLogs';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 interface PodScope {
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
clusterName: string;
|
||||
}
|
||||
|
||||
export interface ContainerScope extends PodScope {
|
||||
containerName: string;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { kubernetesProxyApiRef } from '@backstage/plugin-kubernetes';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { ContainerScope } from './types';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
interface PodLogsOptions {
|
||||
podScope: ContainerScope;
|
||||
}
|
||||
|
||||
export const usePodLogs = ({ podScope }: PodLogsOptions) => {
|
||||
const kubernetesProxyApi = useApi(kubernetesProxyApiRef);
|
||||
return useAsync(async () => {
|
||||
return await kubernetesProxyApi.getPodLogs({
|
||||
podName: podScope.podName,
|
||||
namespace: podScope.podNamespace,
|
||||
containerName: podScope.containerName,
|
||||
clusterName: podScope.clusterName,
|
||||
});
|
||||
}, [JSON.stringify(podScope)]);
|
||||
};
|
||||
+27
-18
@@ -26,6 +26,7 @@ import {
|
||||
} from '../../utils/pod';
|
||||
import { Table, TableColumn } from '@backstage/core-components';
|
||||
import { PodNamesWithMetricsContext } from '../../hooks/PodNamesWithMetrics';
|
||||
import { ClusterContext } from '../../hooks/Cluster';
|
||||
|
||||
export const READY_COLUMNS: PodColumns = 'READY';
|
||||
export const RESOURCE_COLUMNS: PodColumns = 'RESOURCE';
|
||||
@@ -37,23 +38,6 @@ type PodsTablesProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const DEFAULT_COLUMNS: TableColumn<V1Pod>[] = [
|
||||
{
|
||||
title: 'name',
|
||||
highlight: true,
|
||||
render: (pod: V1Pod) => <PodDrawer pod={pod} />,
|
||||
},
|
||||
{
|
||||
title: 'phase',
|
||||
render: (pod: V1Pod) => pod.status?.phase ?? 'unknown',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'status',
|
||||
render: containerStatuses,
|
||||
},
|
||||
];
|
||||
|
||||
const READY: TableColumn<V1Pod>[] = [
|
||||
{
|
||||
title: 'containers ready',
|
||||
@@ -72,7 +56,32 @@ const READY: TableColumn<V1Pod>[] = [
|
||||
|
||||
export const PodsTable = ({ pods, extraColumns = [] }: PodsTablesProps) => {
|
||||
const podNamesWithMetrics = useContext(PodNamesWithMetricsContext);
|
||||
const columns: TableColumn<V1Pod>[] = [...DEFAULT_COLUMNS];
|
||||
const cluster = useContext(ClusterContext);
|
||||
const defaultColumns: TableColumn<V1Pod>[] = [
|
||||
{
|
||||
title: 'name',
|
||||
highlight: true,
|
||||
render: (pod: V1Pod) => (
|
||||
<PodDrawer
|
||||
podAndErrors={{
|
||||
pod: pod as any,
|
||||
clusterName: cluster.name,
|
||||
errors: [],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'phase',
|
||||
render: (pod: V1Pod) => pod.status?.phase ?? 'unknown',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'status',
|
||||
render: containerStatuses,
|
||||
},
|
||||
];
|
||||
const columns: TableColumn<V1Pod>[] = [...defaultColumns];
|
||||
|
||||
if (extraColumns.includes(READY_COLUMNS)) {
|
||||
columns.push(...READY);
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { Pod } from 'kubernetes-models/v1';
|
||||
|
||||
export interface PodAndErrors {
|
||||
clusterName: string;
|
||||
pod: Pod;
|
||||
errors: any[];
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { V1Service } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
|
||||
export const ServiceDrawer = ({
|
||||
@@ -27,7 +27,7 @@ export const ServiceDrawer = ({
|
||||
expanded?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={service}
|
||||
expanded={expanded}
|
||||
kind="Service"
|
||||
@@ -53,6 +53,6 @@ export const ServiceDrawer = ({
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { V1StatefulSet } from '@kubernetes/client-node';
|
||||
import { KubernetesDrawer } from '../KubernetesDrawer/KubernetesDrawer';
|
||||
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
|
||||
import { renderCondition } from '../../utils/pod';
|
||||
import { Typography, Grid, Chip } from '@material-ui/core';
|
||||
|
||||
@@ -29,7 +29,7 @@ export const StatefulSetDrawer = ({
|
||||
}) => {
|
||||
const namespace = statefulset.metadata?.namespace;
|
||||
return (
|
||||
<KubernetesDrawer
|
||||
<KubernetesStructuredMetadataTableDrawer
|
||||
object={statefulset}
|
||||
expanded={expanded}
|
||||
kind="StatefulSet"
|
||||
@@ -74,6 +74,6 @@ export const StatefulSetDrawer = ({
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</KubernetesDrawer>
|
||||
</KubernetesStructuredMetadataTableDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export * from './ErrorReporting';
|
||||
export * from './HorizontalPodAutoscalers';
|
||||
export * from './IngressesAccordions';
|
||||
export * from './JobsAccordions';
|
||||
export { KubernetesDrawer } from './KubernetesDrawer';
|
||||
export * from './KubernetesDrawer';
|
||||
export * from './Pods';
|
||||
export * from './ServicesAccordions';
|
||||
export * from './KubernetesContent';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
|
||||
import { kubernetesApiRef } from './api/types';
|
||||
import { kubernetesApiRef, kubernetesProxyApiRef } from './api/types';
|
||||
import { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types';
|
||||
import { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders';
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
oneloginAuthApiRef,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { KubernetesProxyClient } from './api';
|
||||
|
||||
export const rootCatalogKubernetesRouteRef = createRouteRef({
|
||||
id: 'kubernetes',
|
||||
@@ -52,6 +53,16 @@ export const kubernetesPlugin = createPlugin({
|
||||
kubernetesAuthProvidersApi,
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesProxyApiRef,
|
||||
deps: {
|
||||
kubernetesApi: kubernetesApiRef,
|
||||
},
|
||||
factory: ({ kubernetesApi }) =>
|
||||
new KubernetesProxyClient({
|
||||
kubernetesApi,
|
||||
}),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesAuthProvidersApiRef,
|
||||
deps: {
|
||||
|
||||
Reference in New Issue
Block a user