UX Trace Screen (#1781)

This commit is contained in:
Prakash Senthil Vel
2022-03-31 19:05:10 +00:00
committed by GitHub
parent aaa55a1f4a
commit 461bc94a0b
6 changed files with 584 additions and 244 deletions

View File

@@ -0,0 +1,141 @@
// 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, { SVGProps } from "react";
const FilterIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
width="14"
height="13.088"
viewBox="0 0 14 13.088"
{...props}
>
<g id="filter-icon.a949c200" transform="translate(-231.827 -340.123)">
<line
id="Línea_659"
data-name="Línea 659"
x2="14"
transform="translate(231.827 346.667)"
fill="none"
stroke="#434343"
strokeWidth="1"
/>
<g
id="Grupo_2472"
data-name="Grupo 2472"
transform="translate(240.693 344.614)"
>
<circle
id="Elipse_611"
data-name="Elipse 611"
cx="2.053"
cy="2.053"
r="2.053"
transform="translate(0 0)"
fill="#fff"
/>
<circle
id="Elipse_612"
data-name="Elipse 612"
cx="1.597"
cy="1.597"
r="1.597"
transform="translate(0.456 0.456)"
fill="none"
stroke="#414141"
strokeWidth="1"
/>
</g>
<line
id="Línea_660"
data-name="Línea 660"
x2="14"
transform="translate(231.827 342.22)"
fill="none"
stroke="#434343"
strokeWidth="1"
/>
<g
id="Grupo_2473"
data-name="Grupo 2473"
transform="translate(232.394 340.167)"
>
<circle
id="Elipse_613"
data-name="Elipse 613"
cx="2.053"
cy="2.053"
r="2.053"
transform="translate(0 0)"
fill="#fff"
/>
<circle
id="Elipse_614"
data-name="Elipse 614"
cx="1.597"
cy="1.597"
r="1.597"
transform="translate(0.456 0.456)"
fill="none"
stroke="#414141"
strokeWidth="1"
/>
</g>
<line
id="Línea_661"
data-name="Línea 661"
x2="14"
transform="translate(231.827 351.114)"
fill="none"
stroke="#434343"
strokeWidth="1"
/>
<g
id="Grupo_2474"
data-name="Grupo 2474"
transform="translate(235.161 349.061)"
>
<circle
id="Elipse_615"
data-name="Elipse 615"
cx="2.053"
cy="2.053"
r="2.053"
transform="translate(0 0)"
fill="#fff"
/>
<circle
id="Elipse_616"
data-name="Elipse 616"
cx="1.597"
cy="1.597"
r="1.597"
transform="translate(0.456 0.456)"
fill="none"
stroke="#414141"
strokeWidth="1"
/>
</g>
</g>
</svg>
);
};
export default FilterIcon;

View File

@@ -181,3 +181,4 @@ export { default as LicenseDocIcon } from "./LicenseDocIcon";
export { default as SelectAllIcon } from "./SelectAllIcon";
export { default as BackIcon } from "./BackIcon";
export { default as DeleteNonCurrentIcon } from "./DeleteNonCurrentIcon";
export { default as FilterIcon } from "./FilterIcon";

View File

@@ -79,11 +79,13 @@ const RBIconButton = (props: RBIconProps) => {
disabled = false,
tooltip,
icon = null,
className = "",
...restProps
} = props;
return (
<BoxIconButton
className={className}
classes={classes}
tooltip={tooltip || text}
variant="outlined"

View File

@@ -1093,6 +1093,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
<br />
BackIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.FilterIcon />
<br />
FilterIcon
</Grid>
</Grid>
<h1>Menu Icons</h1>
<Grid

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react";
import { Button, Grid, TextField } from "@mui/material";
import { Box, Grid } from "@mui/material";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
@@ -43,6 +43,9 @@ import PageHeader from "../Common/PageHeader/PageHeader";
import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import moment from "moment/moment";
import PageLayout from "../Common/Layout/PageLayout";
import { FilterIcon } from "../../../icons";
import RBIconButton from "../Buckets/BucketDetails/SummaryItems/RBIconButton";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
const styles = (theme: Theme) =>
createStyles({
@@ -89,9 +92,18 @@ const styles = (theme: Theme) =>
},
formBox: {
border: "1px solid #EAEAEA",
padding: 15,
padding: 25,
marginBottom: 15,
},
traceCheckedIcon: {
width: "14px",
height: "14px",
marginLeft: "0px",
},
unCheckedIcon: {
width: "14px",
height: "14px",
},
midColumnCheckboxes: {
display: "flex",
},
@@ -139,6 +151,8 @@ const Trace = ({
const [os, setOS] = useState<boolean>(false);
const [errors, setErrors] = useState<boolean>(false);
const [toggleFilter, setToggleFilter] = useState<boolean>(false);
const startTrace = () => {
traceResetMessages();
const url = new URL(window.location.toString());
@@ -201,240 +215,417 @@ const Trace = ({
<Fragment>
<PageHeader label={"Trace"} />
<PageLayout>
<Grid xs={12} className={classes.formBox}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
className={classes.searchField}
id="status-code"
label="Status Code"
InputProps={{
disableUnderline: true,
}}
value={statusCode}
onChange={(e) => {
setStatusCode(e.target.value);
}}
disabled={traceStarted}
variant="standard"
/>
<TextField
className={classes.searchField}
id="method"
label="Method"
InputProps={{
disableUnderline: true,
}}
value={method}
onChange={(e) => {
setMethod(e.target.value);
}}
disabled={traceStarted}
variant="standard"
/>
<TextField
label="Function Name"
className={classes.searchField}
id="func-name"
disabled={traceStarted}
InputProps={{
disableUnderline: true,
}}
value={func}
onChange={(e) => {
setFunc(e.target.value);
}}
variant="standard"
/>
<TextField
className={classes.searchField}
id="path"
label="Path"
disabled={traceStarted}
InputProps={{
disableUnderline: true,
}}
value={path}
onChange={(e) => {
setPath(e.target.value);
}}
variant="standard"
/>
<TextField
type="number"
className={classes.searchField}
id="fthreshold"
label="Response Threshold"
disabled={traceStarted}
InputProps={{
disableUnderline: true,
}}
inputProps={{
min: 0,
}}
value={threshold}
onChange={(e) => {
setThreshold(parseInt(e.target.value));
}}
variant="standard"
/>
</Grid>
<Grid item xs={12} className={classes.inlineCheckboxes}>
<div className={classes.checkBoxLabel}>Calls to trace:</div>
<div className={classes.midColumnCheckboxes}>
<CheckboxWrapper
checked={all}
id={"all_calls"}
name={"all_calls"}
label={"All"}
onChange={(item) => {
setAll(item.target.checked);
}}
value={"all"}
disabled={traceStarted}
/>
<CheckboxWrapper
checked={s3 || all}
id={"s3_calls"}
name={"s3_calls"}
label={"S3"}
onChange={(item) => {
setS3(item.target.checked);
}}
value={"s3"}
disabled={all || traceStarted}
/>
<CheckboxWrapper
checked={internal || all}
id={"internal_calls"}
name={"internal_calls"}
label={"Internal"}
onChange={(item) => {
setInternal(item.target.checked);
}}
value={"internal"}
disabled={all || traceStarted}
/>
<CheckboxWrapper
checked={storage || all}
id={"storage_calls"}
name={"storage_calls"}
label={"Storage"}
onChange={(item) => {
setStorage(item.target.checked);
}}
value={"storage"}
disabled={all || traceStarted}
/>
<CheckboxWrapper
checked={os || all}
id={"os_calls"}
name={"os_calls"}
label={"OS"}
onChange={(item) => {
setOS(item.target.checked);
}}
value={"os"}
disabled={all || traceStarted}
/>
<span className={classes.separatorBar}>
&nbsp; &nbsp; &nbsp; | &nbsp; &nbsp; &nbsp;
</span>
</div>
<CheckboxWrapper
checked={errors}
id={"only_errors"}
name={"only_errors"}
label={"Display only Errors"}
onChange={(item) => {
setErrors(item.target.checked);
}}
value={"only_errors"}
disabled={traceStarted}
/>
</Grid>
<Grid item xs={12} className={classes.startButton}>
{!traceStarted && (
<Button
type="submit"
variant="contained"
color="primary"
disabled={traceStarted}
onClick={startTrace}
>
Start
</Button>
)}
{traceStarted && (
<Button
type="button"
variant="contained"
color="primary"
onClick={stopTrace}
>
Stop
</Button>
)}
</Grid>
</Grid>
<Grid className={classes.formBox}>
<Grid
item
xs={12}
sx={{
display: "flex",
flexFlow: "column",
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={[]}
columns={[
{
label: "Time",
elementKey: "ptime",
renderFunction: (time: Date) => {
const timeParse = new Date(time);
return timeFromDate(timeParse);
"& .trace-checkbox-label": {
fontSize: "14px",
fontWeight: "normal",
},
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
padding: "20px 0px 20px 0",
}}
>
Calls to Trace
</Box>
<Box
className={`${traceStarted ? "inactive-state" : ""}`}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
"&.inactive-state .trace-checkbox-label": {
color: "#a6a5a5",
},
globalClass: classes.timeItem,
},
{ label: "Name", elementKey: "api" },
{
label: "Status",
elementKey: "",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.statusCode} ${fullElement.statusMsg}`,
renderFullObject: true,
},
{
label: "Location",
elementKey: "configuration_id",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.host} ${fullElement.client}`,
renderFullObject: true,
},
{
label: "Load Time",
elementKey: "callStats.duration",
globalClass: classes.timeItem,
},
{
label: "Upload",
elementKey: "callStats.rx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
{
label: "Download",
elementKey: "callStats.tx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
]}
isLoading={false}
records={messages}
entityName="Traces"
idField="api"
customEmptyMessage={
traceStarted
? "No Traced elements received yet"
: "Trace is not started yet"
}
customPaperHeight={classes.tableWrapper}
autoScrollToBottom
/>
}}
>
<Box
sx={{
display: "flex",
flexFlow: "row",
gap: {
md: "30px",
},
"& .trace-checked-icon": {
border: "1px solid red",
},
}}
>
<CheckboxWrapper
checked={all}
id={"all_calls"}
name={"all_calls"}
label={"All"}
onChange={(item) => {
setAll(item.target.checked);
}}
value={"all"}
disabled={traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
<CheckboxWrapper
checked={s3 || all}
id={"s3_calls"}
name={"s3_calls"}
label={"S3"}
onChange={(item) => {
setS3(item.target.checked);
}}
value={"s3"}
disabled={traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
<CheckboxWrapper
checked={internal || all}
id={"internal_calls"}
name={"internal_calls"}
label={"Internal"}
onChange={(item) => {
setInternal(item.target.checked);
}}
value={"internal"}
disabled={all || traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
<CheckboxWrapper
checked={storage || all}
id={"storage_calls"}
name={"storage_calls"}
label={"Storage"}
onChange={(item) => {
setStorage(item.target.checked);
}}
value={"storage"}
disabled={all || traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
<CheckboxWrapper
checked={os || all}
id={"os_calls"}
name={"os_calls"}
label={"OS"}
onChange={(item) => {
setOS(item.target.checked);
}}
value={"os"}
disabled={all || traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "15px",
}}
>
<RBIconButton
tooltip={"More filter options"}
onClick={() => {
setToggleFilter(!toggleFilter);
}}
text={"Filters"}
icon={<FilterIcon />}
color={"primary"}
variant={"outlined"}
className={"filters-toggle-button"}
style={{
width: "118px",
background: toggleFilter ? "rgba(8, 28, 66, 0.04)" : "",
}}
/>
{!traceStarted && (
<RBIconButton
text={"Start New Trace"}
data-test-id={"trace-start-button"}
icon={null}
color={"primary"}
variant="contained"
onClick={startTrace}
style={{
width: "118px",
}}
/>
)}
{traceStarted && (
<RBIconButton
text={"Stop Trace"}
data-test-id={"trace-stop-button"}
icon={null}
color={"primary"}
variant="contained"
onClick={stopTrace}
style={{
width: "118px",
}}
/>
)}
</Box>
</Box>
</Grid>
{toggleFilter ? (
<Grid
item
className={`${traceStarted ? "inactive-state" : ""}`}
xs={12}
sx={{
marginTop: "25px",
display: "flex",
flexFlow: "column",
background: "#FBFAFA",
padding: "30px",
"&.inactive-state label": {
color: "#a6a5a5",
},
"& .orient-vertical": {
flexFlow: "column",
"& label": {
marginBottom: "10px",
fontWeight: 600,
},
},
"& .trace-checkbox-label": {
fontSize: "14px",
fontWeight: "normal",
},
}}
>
<Box
sx={{
gap: "30px",
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
width: "100%",
}}
>
<InputBoxWrapper
className="orient-vertical"
id="trace-status-code"
name="trace-status-code"
label="Status Code"
classes={{}}
placeholder="e.g. 503"
value={statusCode}
onChange={(e) => {
setStatusCode(e.target.value);
}}
disabled={traceStarted}
/>
<InputBoxWrapper
className="orient-vertical"
id="trace-function-name"
name="trace-function-name"
label="Function Name"
classes={{}}
placeholder="e.g. FunctionName2055"
value={func}
onChange={(e) => {
setFunc(e.target.value);
}}
disabled={traceStarted}
/>
<InputBoxWrapper
className="orient-vertical"
id="trace-method"
name="trace-method"
label="Method"
classes={{}}
placeholder="e.g. Method 2056"
value={method}
onChange={(e) => {
setMethod(e.target.value);
}}
disabled={traceStarted}
/>
</Box>
<Box
sx={{
gap: "30px",
display: "grid",
gridTemplateColumns: "2fr 1fr",
width: "100%",
marginTop: "33px",
}}
>
<Box
flex="2"
style={{
width: "calc( 100% + 10px)",
}}
>
<InputBoxWrapper
className="orient-vertical"
id="trace-path"
name="trace-path"
label="Path"
classes={{}}
placeholder="e.g. my-bucket/my-prefix/*"
value={path}
onChange={(e) => {
setPath(e.target.value);
}}
disabled={traceStarted}
/>
</Box>
<Box
sx={{
marginLeft: "15px",
}}
>
<InputBoxWrapper
className="orient-vertical"
id="trace-fthreshold"
name="trace-fthreshold"
label="Response Threshold"
type="number"
classes={{}}
placeholder="e.g. website.io.3249.114.12"
value={`${threshold}`}
onChange={(e) => {
setThreshold(parseInt(e.target.value));
}}
disabled={traceStarted}
/>
</Box>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
marginTop: "40px",
}}
>
<CheckboxWrapper
checked={errors}
id={"only_errors"}
name={"only_errors"}
label={"Display only Errors"}
onChange={(item) => {
setErrors(item.target.checked);
}}
value={"only_errors"}
disabled={traceStarted}
overrideLabelClasses="trace-checkbox-label"
classes={{
checkedIcon: classes.traceCheckedIcon,
unCheckedIcon: classes.unCheckedIcon,
}}
/>
</Box>
</Grid>
) : null}
<Grid item xs={12}>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
marginBottom: "30px",
marginTop: "30px",
}}
>
Trace Results
</Box>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={[]}
columns={[
{
label: "Time",
elementKey: "ptime",
renderFunction: (time: Date) => {
const timeParse = new Date(time);
return timeFromDate(timeParse);
},
globalClass: classes.timeItem,
},
{ label: "Name", elementKey: "api" },
{
label: "Status",
elementKey: "",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.statusCode} ${fullElement.statusMsg}`,
renderFullObject: true,
},
{
label: "Location",
elementKey: "configuration_id",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.host} ${fullElement.client}`,
renderFullObject: true,
},
{
label: "Load Time",
elementKey: "callStats.duration",
globalClass: classes.timeItem,
},
{
label: "Upload",
elementKey: "callStats.rx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
{
label: "Download",
elementKey: "callStats.tx",
renderFunction: niceBytes,
globalClass: classes.sizeItem,
},
]}
isLoading={false}
records={messages}
entityName="Traces"
idField="api"
customEmptyMessage={
traceStarted
? "No Traced elements received yet"
: "Trace is not started yet"
}
customPaperHeight={classes.tableWrapper}
autoScrollToBottom
/>
</Grid>
</Grid>
</PageLayout>
</Fragment>

View File

@@ -16,11 +16,11 @@
import * as roles from "../utils/roles";
import * as elements from "../utils/elements";
import {
monitoringElement,
supportElement,
traceElement,
} from "../utils/elements-menu";
import { monitoringElement, traceElement } from "../utils/elements-menu";
import { Selector } from "testcafe";
export const traceStartButton = Selector('[data-test-id="trace-start-button"]');
export const traceStopButton = Selector('[data-test-id="trace-stop-button"]');
fixture("For user with Trace permissions")
.page("http://localhost:9090")
@@ -48,14 +48,14 @@ test("Trace page can be opened", async (t) => {
test("Start button can be clicked", async (t) => {
await t
.navigateTo("http://localhost:9090/tools/trace")
.click(elements.startButton);
.click(traceStartButton);
});
test("Stop button appears after Start button has been clicked", async (t) => {
const stopButtonExists = elements.stopButton.exists;
const stopButtonExists = traceStopButton.exists;
await t
.navigateTo("http://localhost:9090/tools/trace")
.click(elements.startButton)
.click(traceStartButton)
.expect(stopButtonExists)
.ok();
});
@@ -63,6 +63,6 @@ test("Stop button appears after Start button has been clicked", async (t) => {
test("Stop button can be clicked after Start button has been clicked", async (t) => {
await t
.navigateTo("http://localhost:9090/tools/trace")
.click(elements.startButton)
.click(elements.stopButton);
.click(traceStartButton)
.click(traceStopButton);
});