Logs Re-Design (#1656)

This commit is contained in:
Daniel Valdivia
2022-03-03 15:18:19 -08:00
committed by GitHub
parent 06bfe52e7a
commit bfaea09c0b
6 changed files with 465 additions and 202 deletions

View File

@@ -0,0 +1,53 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const BoxArrowDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 26 26"
{...props}
>
<g id="Group_2476" data-name="Group 2476" transform="translate(-1898 -343)">
<rect
id="Rectangle_1114"
data-name="Rectangle 1114"
width="26"
height="26"
transform="translate(1898 343)"
fill="#fbfafa"
/>
<g
id="noun_chevron_2320228"
transform="translate(1915.2 353.499) rotate(90)"
>
<path
id="Path_6842"
data-name="Path 6842"
d="M.47,8a.464.464,0,0,1-.329-.141.468.468,0,0,1,0-.67L3.325,4.006.141.811a.468.468,0,0,1,0-.67.468.468,0,0,1,.67,0L4.335,3.665a.464.464,0,0,1,.141.329.427.427,0,0,1-.141.329L.811,7.847A.476.476,0,0,1,.47,8Z"
transform="translate(0 0)"
fill="#2781b0"
/>
</g>
</g>
</svg>
);
export default BoxArrowDown;

View File

@@ -0,0 +1,57 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const BoxArrowUp = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 26 26"
{...props}
>
<g
id="Group_2001"
data-name="Group 2001"
transform="translate(1924 369) rotate(180)"
>
<rect
id="Rectangle_1114"
data-name="Rectangle 1114"
width="26"
height="26"
transform="translate(1898 343)"
fill="#e5e5e5"
/>
<g
id="noun_chevron_2320228"
transform="translate(1915.2 353.499) rotate(90)"
>
<path
id="Path_6842"
data-name="Path 6842"
d="M.47,8a.464.464,0,0,1-.329-.141.468.468,0,0,1,0-.67L3.325,4.006.141.811a.468.468,0,0,1,0-.67.468.468,0,0,1,.67,0L4.335,3.665a.464.464,0,0,1,.141.329.427.427,0,0,1-.141.329L.811,7.847A.476.476,0,0,1,.47,8Z"
transform="translate(0 0)"
fill="#5e5e5e"
/>
</g>
</g>
</svg>
);
export default BoxArrowUp;

View File

@@ -0,0 +1,59 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const WarnFilledIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
id="WarnFilledIcon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12"
{...props}
className={`min-icon`}
fill={"currentcolor"}
>
<defs>
<clipPath id="clip-path">
<rect
id="Rectangle_987"
data-name="Rectangle 987"
width="12"
height="12"
/>
</clipPath>
</defs>
<g id="warning-icon-full" transform="translate(-0.002 -0.003)">
<g
id="Group_2356"
data-name="Group 2356"
transform="translate(0.002 0.003)"
clip-path="url(#clip-path)"
>
<path
id="Path_7081"
data-name="Path 7081"
d="M6,0H6a6,6,0,1,0,6,6A6,6,0,0,0,6,0m.964,1.947L6.751,7.434H5.318L5.1,1.947ZM6.04,10.454a1.134,1.134,0,1,1,0-2.269,1.134,1.134,0,0,1,0,2.269"
transform="translate(-0.002 -0.003)"
/>
</g>
</g>
</svg>
);
};
export default WarnFilledIcon;

View File

@@ -24,7 +24,6 @@ import moment from "moment/moment";
import { AppState } from "../../../../store";
import { logMessageReceived, logResetMessages } from "../actions";
import { LogMessage } from "../types";
import { timeFromDate } from "../../../../common/utils";
import { wsProtocol } from "../../../../utils/wsUtils";
import {
actionsTray,
@@ -35,6 +34,11 @@ import {
import PageHeader from "../../Common/PageHeader/PageHeader";
import PageLayout from "../../Common/Layout/PageLayout";
import SearchBox from "../../Common/SearchBox";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import LogLine from "./LogLine";
const styles = (theme: Theme) =>
createStyles({
@@ -44,7 +48,6 @@ const styles = (theme: Theme) =>
height: "calc(100vh - 280px)",
overflow: "auto",
fontSize: 13,
padding: "15px 15px 0",
border: "1px solid #EAEDEE",
borderRadius: 4,
},
@@ -85,7 +88,7 @@ const ErrorLogs = ({
logResetMessages,
messages,
}: ILogs) => {
const [highlight, setHighlight] = useState("");
const [filter, setFilter] = useState<string>("");
useEffect(() => {
logResetMessages();
@@ -128,218 +131,57 @@ const ErrorLogs = ({
}
}, [logMessageReceived, logResetMessages]);
const renderError = (logElement: LogMessage) => {
let errorElems = [];
if (logElement.error !== null && logElement.error !== undefined) {
if (logElement.api && logElement.api.name) {
const errorText = `API: ${logElement.api.name}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`api-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<br />
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.time) {
const errorText = `Time: ${timeFromDate(logElement.time)}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`time-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.deploymentid) {
const errorText = `DeploymentID: ${logElement.deploymentid}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`deploytmentid-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.requestID) {
const errorText = `RequestID: ${logElement.requestID}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`requestid-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.remotehost) {
const errorText = `RemoteHost: ${logElement.remotehost}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`remotehost-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.host) {
const errorText = `Host: ${logElement.host}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`host-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.userAgent) {
const errorText = `UserAgent: ${logElement.userAgent}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`useragent-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.error.message) {
const errorText = `Error: ${logElement.error.message}`;
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`message-${logElement.key}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror}>{errorText}</span>
</div>
);
}
if (logElement.error.source) {
// for all sources add padding
for (let s in logElement.error.source) {
const errorText = logElement.error.source[s];
const highlightedLine =
highlight !== ""
? errorText.toLowerCase().includes(highlight.toLowerCase())
: false;
errorElems.push(
<div
key={`source-${logElement.key}-${s}`}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.logerror_tab}>{errorText}</span>
</div>
);
}
const filtLow = filter.toLowerCase();
let filteredMessages = messages.filter((m) => {
if (filter !== "") {
if (m.ConsoleMsg.toLowerCase().indexOf(filtLow) >= 0) {
return true;
} else if (
m.error &&
m.error.source &&
m.error.source.filter((x) => {
return x.toLowerCase().indexOf(filtLow) >= 0;
}).length > 0
) {
return true;
} else if (
m.error &&
m.error.message.toLowerCase().indexOf(filtLow) >= 0
) {
return true;
} else if (m.api && m.api.name.toLowerCase().indexOf(filtLow) >= 0) {
return true;
}
return false;
}
return errorElems;
};
const renderLog = (logElement: LogMessage) => {
let logMessage = logElement.ConsoleMsg;
// 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={logElement.key}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.tab}>{substr}</span>
</div>
);
} else if (logElement.error !== null && logElement.error !== undefined) {
// list error message and all sources and error elems
return renderError(logElement);
} else {
// for all remaining set default class
return (
<div
key={logElement.key}
className={`${highlightedLine ? classes.highlight : ""}`}
>
<span className={classes.ansidefault}>{substr}</span>
</div>
);
}
};
const renderLines = messages.map((m) => {
return renderLog(m);
return true;
});
return (
<Fragment>
<PageHeader label="Logs" />
<PageLayout>
<Grid xs={12}>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Highlight Line"
onChange={setHighlight}
value={highlight}
placeholder="Filter"
onChange={(e) => {
setFilter(e);
}}
value={filter}
/>
</Grid>
<Grid item xs={12}>
<div id="logs-container" className={classes.logList}>
{renderLines}
<TableContainer component={Paper}>
<Table aria-label="collapsible table">
<TableBody>
{filteredMessages.map((m) => {
return <LogLine log={m} />;
})}
</TableBody>
</Table>
</TableContainer>
</div>
</Grid>
</Grid>

View File

@@ -0,0 +1,232 @@
// 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, { Fragment, useState } from "react";
import { LogMessage } from "../types";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Collapse from "@mui/material/Collapse";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Moment from "react-moment";
import BoxArrowUp from "../../../../icons/BoxArrowUp";
import BoxArrowDown from "../../../../icons/BoxArrowDown";
import WarnFilledIcon from "../../../../icons/WarnFilledIcon";
const messageForConsoleMsg = (log: LogMessage) => {
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
let fullMessage = log.ConsoleMsg;
// remove the 0x1B character
/* eslint-disable no-control-regex */
fullMessage = fullMessage.replace(/\x1B/g, " ");
/* eslint-enable no-control-regex */
// 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.
fullMessage = fullMessage.replace(tColorRegex, "");
return (
<div
style={{
display: "table",
tableLayout: "fixed",
width: "100%",
paddingLeft: 10,
paddingRight: 10,
}}
>
<div
style={{
display: "table-cell",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflowX: "auto",
}}
>
<pre>{fullMessage}</pre>
</div>
</div>
);
};
const messageForError = (log: LogMessage) => {
const dataStyle = { color: "green" };
return (
<Fragment>
<div>
<b>API:&nbsp;</b>
<span style={dataStyle}>{log.api.name}</span>
</div>
<div>
<b>Time:&nbsp;</b>
<span style={dataStyle}>{log.time.toString()}</span>
</div>
<div>
<b>DeploymentID:&nbsp;</b>
<span style={dataStyle}>{log.deploymentid}</span>
</div>
<div>
<b>RequestID:&nbsp;</b>
<span style={dataStyle}>{log.requestID}</span>
</div>
<div>
<b>RemoteHost:&nbsp;</b>
<span style={dataStyle}>{log.remotehost}</span>
</div>
<div>
<b>UserAgent:&nbsp;</b>
<span style={dataStyle}>{log.userAgent}</span>
</div>
<div>
<b>Error:&nbsp;</b>
<span style={dataStyle}>{log.error && log.error.message}</span>
</div>
<br />
<div>
<b>Backtrace:&nbsp;</b>
</div>
{log.error &&
log.error.source.map((e, i) => {
return (
<div>
<b>{i}:&nbsp;</b>
<span style={dataStyle}>{e}</span>
</div>
);
})}
</Fragment>
);
};
const LogLine = (props: { log: LogMessage }) => {
const { log } = props;
const [open, setOpen] = useState<boolean>(false);
let logMessage = "";
if (log.ConsoleMsg !== "") {
logMessage = log.ConsoleMsg;
} else if (log.error !== null && log.error.message !== "") {
logMessage = log.error.message;
}
// remove any non ascii characters, exclude any control codes
let titleLogMessage = logMessage.replace(/━|┏|┓|┃|┗|┛/g, "");
// remove any non ascii characters, exclude any control codes
titleLogMessage = titleLogMessage.replace(/([^\x20-\x7F])/g, "");
// regex for terminal colors like e.g. `[31;4m `
const tColorRegex = /((\[[0-9;]+m))/g;
let fullMessage = <Fragment />;
if (log.ConsoleMsg !== "") {
fullMessage = messageForConsoleMsg(log);
} else if (log.error !== null && log.error.message !== "") {
fullMessage = messageForError(log);
}
titleLogMessage = titleLogMessage.replace(tColorRegex, "");
let dateStr = <Moment format="YYYY/MM/DD UTC HH:mm:ss">{log.time}</Moment>;
if (log.time.getFullYear() === 1) {
dateStr = <Fragment>n/a</Fragment>;
}
return (
<React.Fragment key={log.time.toString()}>
<TableRow
sx={{ "& > *": { borderBottom: "unset" }, cursor: "pointer" }}
style={{ backgroundColor: "#FDFDFD" }}
>
<TableCell
onClick={() => setOpen(!open)}
style={{ width: 200, color: "#989898", fontSize: 12 }}
>
<Box
sx={{
"& .min-icon": { width: 12, marginRight: 1 },
fontWeight: "bold",
lineHeight: 1,
}}
>
<WarnFilledIcon />
{dateStr}
</Box>
</TableCell>
<TableCell onClick={() => setOpen(!open)}>
<div
style={{
display: "table",
tableLayout: "fixed",
width: "100%",
paddingLeft: 10,
paddingRight: 10,
}}
>
<div
style={{
display: "table-cell",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{titleLogMessage}
</div>
</div>
</TableCell>
<TableCell onClick={() => setOpen(!open)} style={{ width: 40 }}>
{open ? <BoxArrowUp /> : <BoxArrowDown />}
</TableCell>
</TableRow>
<TableRow>
<TableCell
style={{
paddingBottom: 0,
paddingTop: 0,
width: 200,
textTransform: "uppercase",
verticalAlign: "top",
textAlign: "right",
color: "#8399AB",
fontWeight: "bold",
}}
>
<Collapse in={open} timeout="auto" unmountOnExit>
<div style={{ marginTop: 10 }}>Log Details</div>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography
style={{
background: "#efefef",
border: "1px solid #dedede",
padding: 4,
fontSize: 14,
color: "#666666",
}}
>
{fullMessage}
</Typography>
</Box>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0, width: 40 }} />
</TableRow>
</React.Fragment>
);
};
export default LogLine;

View File

@@ -35,9 +35,29 @@ export function logReducer(
): LogState {
switch (action.type) {
case LOG_MESSAGE_RECEIVED:
// if it's a simple ConsoleMsg, append it to the current ConsoleMsg in the
// state if any
let msgs = [...state.messages];
if (
msgs.length > 0 &&
action.message.time.getFullYear() === 1 &&
action.message.ConsoleMsg !== ""
) {
for (let m in msgs) {
if (msgs[m].time.getFullYear() === 1) {
msgs[
m
].ConsoleMsg = `${msgs[m].ConsoleMsg}\n${action.message.ConsoleMsg}`;
}
}
} else {
msgs.push(action.message);
}
return {
...state,
messages: [...state.messages, action.message],
messages: msgs,
};
case LOG_RESET_MESSAGES:
return {