Re-organize Pod Details (#821)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2021-06-17 19:12:52 -07:00
committed by GitHub
parent 85797749ba
commit 2d6b5ecbc6
6 changed files with 471 additions and 345 deletions

View File

@@ -6,6 +6,7 @@ import Typography from "@material-ui/core/Typography";
interface IPageHeader {
classes: any;
label: any;
actions?: any;
}
const styles = (theme: Theme) =>
@@ -31,16 +32,29 @@ const styles = (theme: Theme) =>
marginLeft: 55,
marginTop: 8,
},
rightMenu: {
marginTop: 16,
marginRight: 8,
},
});
const PageHeader = ({ classes, label }: IPageHeader) => {
const PageHeader = ({ classes, label, actions }: IPageHeader) => {
return (
<Grid container className={classes.headerContainer}>
<Grid item xs={12} className={classes.label}>
<Grid
container
className={classes.headerContainer}
justify={"space-between"}
>
<Grid item className={classes.label}>
<Typography variant="h4" className={classes.labelStyle}>
{label}
</Typography>
</Grid>
{actions && (
<Grid item className={classes.rightMenu}>
{actions}
</Grid>
)}
</Grid>
);
};

View File

@@ -56,7 +56,7 @@ import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import HealthInfo from "./HealthInfo/HealthInfo";
import Storage from "./Storage/Storage";
import PodDetails from "./Tenants/TenantDetails/PodDetails";
import PodDetails from "./Tenants/TenantDetails/pods/PodDetails";
const drawerWidth = 245;

View File

@@ -1,341 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
buttonsStyles,
containerForHeader,
hrClass,
modalBasic,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import { TextField } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import Paper from "@material-ui/core/Paper";
import api from "../../../../common/api";
import PageHeader from "../../Common/PageHeader/PageHeader";
import { IEvent } from "../ListTenants/types";
import { Link } from "react-router-dom";
import { setErrorSnackMessage } from "../../../../actions";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import { niceDays } from "../../../../common/utils";
interface ITenantDetailsProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "#fff",
minHeight: 400,
height: "calc(100vh - 304px)",
overflow: "auto",
fontSize: 13,
padding: "25px 45px 0",
border: "1px solid #EAEDEE",
borderRadius: 4,
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex",
justifyContent: "space-between",
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid",
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
updateButton: {
backgroundColor: "transparent",
border: 0,
padding: "0 6px",
cursor: "pointer",
"&:focus, &:active": {
outline: "none",
},
"& svg": {
height: 12,
},
},
poolLabel: {
color: "#666666",
},
titleCol: {
fontWeight: "bold",
},
breadcrumLink: {
textDecoration: "none",
color: "black",
},
...modalBasic,
...actionsTray,
...buttonsStyles,
...searchField,
...hrClass,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
logerror: {
color: "#A52A2A",
},
logerror_tab: {
color: "#A52A2A",
paddingLeft: 25,
},
ansidefault: {
color: "#000",
},
highlight: {
"& span": {
backgroundColor: "#082F5238",
},
},
...containerForHeader(theme.spacing(4)),
});
const TenantDetails = ({
classes,
match,
setErrorSnackMessage,
}: ITenantDetailsProps) => {
const [event, setEvent] = useState<IEvent[]>([]);
const [curTab, setCurTab] = useState<number>(0);
const [highlight, setHighlight] = useState<string>("");
const [logLines, setLogLines] = useState<string[]>([]);
const tenantNamespace = match.params["tenantNamespace"];
const tenantName = match.params["tenantName"];
const podName = match.params["podName"];
const renderLog = (logMessage: string, index: number) => {
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.replace(tColorRegex, "");
// in case highlight is set, we select the line that contains the requested string
let highlightedLine =
highlight !== ""
? logMessage.toLowerCase().includes(highlight.toLowerCase())
: false;
// if starts with multiple spaces add padding
if (substr.startsWith(" ")) {
return (
<div
key={index}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.tab}>{substr}</span>
</div>
);
} else {
// for all remaining set default class
return (
<div
key={index}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.ansidefault}>{substr}</span>
</div>
);
}
};
const renderLines = logLines.map((m, i) => {
return renderLog(m, i);
});
function a11yProps(index: any) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
useEffect(() => {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pods/${podName}`
)
.then((res: string) => {
setLogLines(res.split("\n"));
})
.catch((err) => {
setErrorSnackMessage(err);
});
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/pods/${podName}/events`
)
.then((res: IEvent[]) => {
for (let i = 0; i < res.length; i++) {
let currentTime = (Date.now() / 1000) | 0;
res[i].seen = niceDays((currentTime - res[i].last_seen).toString());
}
setEvent(res);
})
.catch((err) => {
setErrorSnackMessage(err);
});
}, [podName, tenantName, tenantNamespace, setErrorSnackMessage]);
return (
<React.Fragment>
<PageHeader
label={
<Fragment>
<Link to={"/tenants"} className={classes.breadcrumLink}>
Tenants
</Link>
{" > "}
<Link
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
className={classes.breadcrumLink}
>
{tenantName}
</Link>
{` > Pods > ${podName}`}
</Fragment>
}
/>
<Grid item xs={12} className={classes.container} />
<Grid container>
<Grid item xs={9}>
<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="Logs" {...a11yProps(0)} />
<Tab label="Events" {...a11yProps(1)} />
</Tabs>
</Grid>
{curTab === 0 && (
<Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Highlight Line"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setHighlight(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper>
<div className={classes.logList}>{renderLines}</div>
</Paper>
</Grid>
</Fragment>
)}
{curTab === 1 && (
<Grid item xs={12} className={classes.actionsTray}>
<TableWrapper
itemActions={[]}
columns={[
{ label: "Namespace", elementKey: "namespace" },
{ label: "Last Seen", elementKey: "seen" },
{ label: "Message", elementKey: "message" },
{ label: "Event Type", elementKey: "event_type" },
{ label: "Reason", elementKey: "reason" },
]}
isLoading={false}
records={event}
entityName="Events"
idField="event"
/>
</Grid>
)}
</Grid>
</React.Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(TenantDetails));

View File

@@ -0,0 +1,136 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import { IconButton } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import PageHeader from "../../../Common/PageHeader/PageHeader";
import { Link } from "react-router-dom";
import { setErrorSnackMessage } from "../../../../../actions";
import RefreshIcon from "@material-ui/icons/Refresh";
import PodLogs from "./PodLogs";
import PodEvents from "./PodEvents";
interface IPodDetailsProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
breadcrumLink: {
textDecoration: "none",
color: "black",
},
...containerForHeader(theme.spacing(4)),
});
const PodDetails = ({ classes, match }: IPodDetailsProps) => {
const [curTab, setCurTab] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const tenantNamespace = match.params["tenantNamespace"];
const tenantName = match.params["tenantName"];
const podName = match.params["podName"];
function a11yProps(index: any) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
useEffect(() => {
if (loading) {
setLoading(false);
}
}, [loading]);
return (
<React.Fragment>
<PageHeader
label={
<Fragment>
<Link to={"/tenants"} className={classes.breadcrumLink}>
Tenants
</Link>
{" > "}
<Link
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
className={classes.breadcrumLink}
>
{tenantName}
</Link>
{` > Pods > ${podName}`}
</Fragment>
}
actions={
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
setLoading(true);
}}
>
<RefreshIcon />
</IconButton>
}
/>
<Grid item xs={12} className={classes.container} />
<Grid container>
<Grid item xs={9}>
<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="Events" {...a11yProps(0)} />
<Tab label="Logs" {...a11yProps(1)} />
</Tabs>
</Grid>
{curTab === 0 && (
<PodEvents
tenant={tenantName}
namespace={tenantNamespace}
podName={podName}
propLoading={loading}
/>
)}
{curTab === 1 && (
<PodLogs
tenant={tenantName}
namespace={tenantNamespace}
podName={podName}
propLoading={loading}
/>
)}
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(PodDetails);

View File

@@ -0,0 +1,120 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
buttonsStyles,
hrClass,
searchField,
} from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import TableWrapper from "../../../Common/TableWrapper/TableWrapper";
import api from "../../../../../common/api";
import { IEvent } from "../../ListTenants/types";
import { setErrorSnackMessage } from "../../../../../actions";
import { niceDays } from "../../../../../common/utils";
interface IPodEventsProps {
classes: any;
tenant: string;
namespace: string;
podName: string;
propLoading: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
...actionsTray,
...buttonsStyles,
...searchField,
...hrClass,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
});
const PodEvents = ({
classes,
tenant,
namespace,
podName,
propLoading,
setErrorSnackMessage,
}: IPodEventsProps) => {
const [event, setEvent] = useState<IEvent[]>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (propLoading) {
setLoading(true);
}
}, [propLoading]);
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/tenants/${tenant}/pods/${podName}/events`
)
.then((res: IEvent[]) => {
for (let i = 0; i < res.length; i++) {
let currentTime = (Date.now() / 1000) | 0;
res[i].seen = niceDays((currentTime - res[i].last_seen).toString());
}
setEvent(res);
setLoading(false);
})
.catch((err) => {
setErrorSnackMessage(err);
setLoading(false);
});
}
}, [loading, podName, namespace, tenant, setErrorSnackMessage]);
return (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TableWrapper
itemActions={[]}
columns={[
{ label: "Namespace", elementKey: "namespace" },
{ label: "Last Seen", elementKey: "seen" },
{ label: "Message", elementKey: "message" },
{ label: "Event Type", elementKey: "event_type" },
{ label: "Reason", elementKey: "reason" },
]}
isLoading={loading}
records={event}
entityName="Events"
idField="event"
/>
</Grid>
</React.Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(PodEvents));

View File

@@ -0,0 +1,197 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
actionsTray,
buttonsStyles,
containerForHeader,
searchField,
} from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import { TextField } from "@material-ui/core";
import Paper from "@material-ui/core/Paper";
import api from "../../../../../common/api";
import { setErrorSnackMessage } from "../../../../../actions";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
interface IPodLogsProps {
classes: any;
tenant: string;
namespace: string;
podName: string;
propLoading: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
logList: {
background: "#fff",
minHeight: 400,
height: "calc(100vh - 304px)",
overflow: "auto",
fontSize: 13,
padding: "25px 45px 0",
border: "1px solid #EAEDEE",
borderRadius: 4,
},
...buttonsStyles,
...searchField,
actionsTray: {
...actionsTray.actionsTray,
padding: "15px 0 0",
},
logerror: {
color: "#A52A2A",
},
logerror_tab: {
color: "#A52A2A",
paddingLeft: 25,
},
ansidefault: {
color: "#000",
},
highlight: {
"& span": {
backgroundColor: "#082F5238",
},
},
...containerForHeader(theme.spacing(4)),
});
const PodLogs = ({
classes,
tenant,
namespace,
podName,
propLoading,
setErrorSnackMessage,
}: IPodLogsProps) => {
const [highlight, setHighlight] = useState<string>("");
const [logLines, setLogLines] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (propLoading) {
setLoading(true);
}
}, [propLoading]);
const renderLog = (logMessage: string, index: number) => {
// remove any non ascii characters, exclude any control codes
logMessage = logMessage.replace(/([^\x20-\x7F])/g, "");
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
// get substring if there was a match for to split what
// is going to be colored and what not, here we add color
// only to the first match.
let substr = logMessage.replace(tColorRegex, "");
// in case highlight is set, we select the line that contains the requested string
let highlightedLine =
highlight !== ""
? logMessage.toLowerCase().includes(highlight.toLowerCase())
: false;
// if starts with multiple spaces add padding
if (substr.startsWith(" ")) {
return (
<div
key={index}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.tab}>{substr}</span>
</div>
);
} else {
// for all remaining set default class
return (
<div
key={index}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.ansidefault}>{substr}</span>
</div>
);
}
};
const renderLines = logLines.map((m, i) => {
return renderLog(m, i);
});
useEffect(() => {
if (loading) {
api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/tenants/${tenant}/pods/${podName}`
)
.then((res: string) => {
setLogLines(res.split("\n"));
setLoading(false);
})
.catch((err) => {
setErrorSnackMessage(err);
setLoading(false);
});
}
}, [loading, podName, namespace, tenant, setErrorSnackMessage]);
return (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Highlight Line"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setHighlight(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper>
<div className={classes.logList}>{renderLines}</div>
</Paper>
</Grid>
</React.Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(PodLogs));