Add describe pod section to console UI (#2001)

This commit is contained in:
Javier Adriel
2022-05-15 21:33:05 -05:00
committed by GitHub
parent 076e44e39a
commit 6e4b8884e6
2 changed files with 465 additions and 1 deletions

View File

@@ -0,0 +1,454 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
actionsTray,
buttonsStyles,
hrClass,
searchField,
} from "../../../Common/FormComponents/common/styleLibrary";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid";
import Chip from "@mui/material/Chip";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import TableContainer from "@mui/material/TableContainer";
import Paper from "@mui/material/Paper";
import { setErrorSnackMessage } from "../../../../../actions";
import { ErrorResponseHandler } from "../../../../../common/types";
import api from "../../../../../common/api";
import { AppState } from "../../../../../store";
import LabelValuePair from "../../../Common/UsageBarWrapper/LabelValuePair";
interface IPodEventsProps {
classes: any;
tenant: string;
namespace: string;
podName: string;
propLoading: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
loadingTenant: boolean;
}
interface Annotation {
key: string;
value: string;
}
interface Condition {
status: string;
type: string;
}
interface EnvVar {
key: string;
value: string;
}
interface Mount {
mountPath: string;
name: string;
}
interface State {
started: string;
state: string;
}
interface Container {
args: string[];
containerID: string;
environmentVariables: EnvVar[];
hostPorts: string[];
image: string;
imageID: string;
lastState: any;
mounts: Mount[];
name: string;
ports: string[];
ready: boolean;
state: State
}
interface Label {
key: string;
value: string;
}
interface Toleration {
effect: string;
key: string;
operator: string;
tolerationSeconds: number;
}
interface VolumePVC{
claimName: string;
}
interface Volume {
name: string;
pvc?: VolumePVC;
projected?: any;
}
interface DescribeResponse {
annotations: Annotation[];
conditions: Condition[];
containers: Container[];
controllerRef: string;
labels: Label[];
name: string;
namespace: string;
nodeName: string;
nodeSelector: string[];
phase: string;
podIP: string;
qosClass: string;
startTime: string;
tolerations: Toleration[];
volumes: Volume[];
}
interface IPodDescribeSummaryProps {
describeInfo: DescribeResponse;
}
interface IPodDescribeAnnotationsProps {
annotations: Annotation[];
}
interface IPodDescribeLabelsProps {
labels: Label[];
}
interface IPodDescribeConditionsProps {
conditions: Condition[];
}
interface IPodDescribeTolerationsProps {
tolerations: Toleration[];
}
interface IPodDescribeVolumesProps {
volumes: Volume[];
}
interface IPodDescribeContainersProps {
containers: Container[];
}
interface IPodDescribeTableProps {
title: string;
columns: string[];
columnsLabels: string[];
items: any[];
}
const styles = (theme: Theme) =>
createStyles({
...actionsTray,
...buttonsStyles,
...searchField,
...hrClass,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
const twoColCssGridLayoutConfig = {
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
padding: "15px",
};
const HeaderSection = ({ title }: { title: string }) => {
return (
<Box
sx={{
borderBottom: "1px solid #eaeaea",
margin: 0,
marginBottom: "20px",
}}
>
<h3>{title}</h3>
</Box>
);
};
const PodDescribeSummary = ({describeInfo}: IPodDescribeSummaryProps) => {
return (
<React.Fragment>
<HeaderSection title={"Summary"} />
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<LabelValuePair label={"Name"} value={describeInfo.name} />
<LabelValuePair label={"Namespace"} value={describeInfo.namespace} />
<LabelValuePair label={"Node"} value={describeInfo.nodeName} />
<LabelValuePair label={"Start time"} value={describeInfo.startTime} />
<LabelValuePair label={"Status"} value={describeInfo.phase} />
<LabelValuePair label={"QoS Class"} value={describeInfo.qosClass} />
<LabelValuePair label={"IP"} value={describeInfo.podIP} />
</Box>
</React.Fragment>
);
};
const PodDescribeAnnotations = ({annotations}: IPodDescribeAnnotationsProps) => {
return (
<React.Fragment>
<HeaderSection title={"Annotations"} />
<Box>
{annotations.map((annotation, index) => (
<Chip style={{ margin: "0.5%" }} label={`${annotation.key}: ${annotation.value}`} key={index} />
))}
</Box>
</React.Fragment>
);
};
const PodDescribeLabels = ({labels}: IPodDescribeLabelsProps) => {
return (
<React.Fragment>
<HeaderSection title={"Labels"} />
<Box>
{labels.map((label, index) => (
<Chip style={{ margin: "0.5%" }} label={`${label.key}: ${label.value}`} key={index} />
))}
</Box>
</React.Fragment>
);
};
const PodDescribeConditions = ({conditions}: IPodDescribeConditionsProps) => {
return <PodDescribeTable
title="Conditions"
columns={["type", "status"]}
columnsLabels={["Type", "Status"]}
items={conditions}
/>;
};
const PodDescribeTolerations = ({tolerations}: IPodDescribeTolerationsProps) => {
return <PodDescribeTable
title="Tolerations"
columns={["effect", "key", "operator", "tolerationSeconds"]}
columnsLabels={["Effect", "Key", "Operator", "Seconds of toleration"]}
items={tolerations}
/>;
};
const PodDescribeVolumes = ({volumes}: IPodDescribeVolumesProps) => {
return (
<React.Fragment>
{volumes.map((volume, index) => (
<React.Fragment key={index}>
<HeaderSection title={`Volume ${volume.name}`} />
<Box sx={{ ...twoColCssGridLayoutConfig }}>
{volume.pvc && (
<React.Fragment>
<LabelValuePair label={"Type"} value="Persistant Volume Claim" />
<LabelValuePair label={"Claim Name"} value={volume.pvc.claimName} />
</React.Fragment>
)}
{/* TODO Add component to display projected data (Maybe change API response) */}
{volume.projected && <LabelValuePair label={"Type"} value="Projected" />}
</Box>
</React.Fragment>
))}
</React.Fragment>
);
};
const PodDescribeTable = ({title, items, columns, columnsLabels}: IPodDescribeTableProps) => {
return (
<React.Fragment>
<HeaderSection title={title} />
<Box>
<TableContainer component={Paper}>
<Table aria-label="collapsible table">
<TableHead>
<TableRow>
{columnsLabels.map((label, index) => <TableCell key={index}>{label}</TableCell> )}
</TableRow>
</TableHead>
<TableBody>
{items.map((item, i) => (
<TableRow key={i}>
{columns.map((column, j) => <TableCell key={j}>{item[column]}</TableCell> )}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</React.Fragment>
);
};
const PodDescribeContainers = ({containers}: IPodDescribeContainersProps) => {
return (
<React.Fragment>
{containers.map((container, index) => (
<React.Fragment key={index}>
<HeaderSection title={`Container ${container.name}`} />
<Box style={{ wordBreak: "break-all" }} sx={{ ...twoColCssGridLayoutConfig }} >
<LabelValuePair label={"Image"} value={container.image} />
<LabelValuePair label={"Ready"} value={`${container.ready}`} />
<LabelValuePair label={"Ports"} value={container.ports.join(", ")} />
<LabelValuePair label={"Host Ports"} value={container.hostPorts.join(", ")} />
<LabelValuePair label={"Arguments"} value={container.args.join(", ")} />
<LabelValuePair label={"Started"} value={container.state.started} />
<LabelValuePair label={"State"} value={container.state.state} />
</Box>
<Box style={{ wordBreak: "break-all" }} sx={{ ...twoColCssGridLayoutConfig }} >
<LabelValuePair label={"Image ID"} value={container.imageID} />
<LabelValuePair label={"Container ID"} value={container.containerID} />
</Box>
<PodDescribeTable
title="Mounts"
columns={["name", "mountPath"]}
columnsLabels={["Name", "Mount Path"]}
items={container.mounts}
/>
<PodDescribeTable
title="Environment Variables"
columns={["key", "value"]}
columnsLabels={["Key", "Value"]}
items={container.environmentVariables}
/>
</React.Fragment>
))}
</React.Fragment>
);
};
const PodDescribe = ({
classes,
tenant,
namespace,
podName,
propLoading,
setErrorSnackMessage,
loadingTenant,
}: IPodEventsProps) => {
const [describeInfo, setDescribeInfo] = useState<DescribeResponse>();
const [loading, setLoading] = useState<boolean>(true);
const [curTab, setCurTab] = useState<number>(0);
useEffect(() => {
if (propLoading) {
setLoading(true);
}
}, [propLoading]);
useEffect(() => {
if (loadingTenant) {
setLoading(true);
}
}, [loadingTenant]);
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/tenants/${tenant}/pods/${podName}/describe`
)
.then((res: DescribeResponse) => {
setDescribeInfo(res);
setLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoading(false);
});
}
}, [loading, podName, namespace, tenant, setErrorSnackMessage]);
const renderTabComponent = (index: number, info: DescribeResponse) => {
switch (index) {
case 0:
return <PodDescribeSummary describeInfo={info} />
case 1:
return <PodDescribeAnnotations annotations={info.annotations} />
case 2:
return <PodDescribeLabels labels={info.labels} />
case 3:
return <PodDescribeConditions conditions={info.conditions} />
case 4:
return <PodDescribeTolerations tolerations={info.tolerations} />
case 5:
return <PodDescribeVolumes volumes={info.volumes} />
case 6:
return <PodDescribeContainers containers={info.containers} />
default:
break;
}
};
return (
<React.Fragment>
{describeInfo && (<Grid item xs={12}>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurTab(newValue);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
variant="scrollable"
scrollButtons="auto">
<Tab label="Summary" />
<Tab label="Annotations" />
<Tab label="Labels" />
<Tab label="Conditions" />
<Tab label="Tolerations" />
<Tab label="Volumes" />
<Tab label="Containers" />
</Tabs>
{renderTabComponent(curTab, describeInfo)}
</Grid>)}
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(PodDescribe));

View File

@@ -26,6 +26,7 @@ import { Link } from "react-router-dom";
import { setErrorSnackMessage } from "../../../../../actions";
import PodLogs from "./PodLogs";
import PodEvents from "./PodEvents";
import PodDescribe from "./PodDescribe";
interface IPodDetailsProps {
classes: any;
@@ -89,7 +90,8 @@ const PodDetails = ({ classes, match }: IPodDetailsProps) => {
scrollButtons="auto"
>
<Tab label="Events" {...a11yProps(0)} />
<Tab label="Logs" {...a11yProps(1)} />
<Tab label="Describe" {...a11yProps(1)} />
<Tab label="Logs" {...a11yProps(2)} />
</Tabs>
</Grid>
{curTab === 0 && (
@@ -101,6 +103,14 @@ const PodDetails = ({ classes, match }: IPodDetailsProps) => {
/>
)}
{curTab === 1 && (
<PodDescribe
tenant={tenantName}
namespace={tenantNamespace}
podName={podName}
propLoading={loading}
/>
)}
{curTab === 2 && (
<PodLogs
tenant={tenantName}
namespace={tenantNamespace}