Re-organize Pod Details (#821)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user