Skip to content

Commit

Permalink
Admin Trace UI (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
dvaldivia committed Apr 30, 2020
1 parent 8e9bd87 commit fe1acaa
Show file tree
Hide file tree
Showing 10 changed files with 437 additions and 101 deletions.
196 changes: 98 additions & 98 deletions portal-ui/bindata_assetfs.go

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion portal-ui/package.json
Expand Up @@ -19,6 +19,7 @@
"@types/recharts": "^1.8.9",
"@types/superagent": "^4.1.4",
"@types/webpack-env": "^1.14.1",
"@types/websocket": "^1.0.0",
"codemirror": "^5.52.2",
"history": "^4.10.1",
"local-storage-fallback": "^4.1.1",
Expand All @@ -37,7 +38,8 @@
"redux-thunk": "^2.3.0",
"superagent": "^5.1.0",
"typeface-roboto": "^0.0.75",
"typescript": "3.6.4"
"typescript": "3.6.4",
"websocket": "^1.0.31"
},
"scripts": {
"start": "PORT=5000 react-scripts start",
Expand Down
2 changes: 2 additions & 0 deletions portal-ui/src/screens/Console/Console.tsx
Expand Up @@ -61,6 +61,7 @@ import ListNotificationEndpoints from "./NotificationEndopoints/ListNotification
import ConfigurationsList from "./Configurations/ConfigurationPanels/ConfigurationsList";
import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";

function Copyright() {
return (
Expand Down Expand Up @@ -302,6 +303,7 @@ class Console extends React.Component<
/>
<Route exact path="/webhook/logger" component={WebhookPanel} />
<Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} />
<Route exact path="/">
<Redirect to="/dashboard" />
</Route>
Expand Down
7 changes: 7 additions & 0 deletions portal-ui/src/screens/Console/Menu.tsx
Expand Up @@ -39,6 +39,7 @@ import PersonIcon from "@material-ui/icons/Person";
import api from "../../common/api";
import NotificationsIcon from "@material-ui/icons/Notifications";
import ListAltIcon from "@material-ui/icons/ListAlt";
import LoopIcon from "@material-ui/icons/Loop";

const styles = (theme: Theme) =>
createStyles({
Expand Down Expand Up @@ -150,6 +151,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon>
<ListItemText primary="IAM Policies" />
</ListItem>
<ListItem button component={NavLink} to="/trace">
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText primary="Trace" />
</ListItem>
<ListItem component={Typography}>Configuration</ListItem>
<ListItem button component={NavLink} to="/notification-endpoints">
<ListItemIcon>
Expand Down
157 changes: 157 additions & 0 deletions portal-ui/src/screens/Console/Trace/Trace.tsx
@@ -0,0 +1,157 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";

const styles = (theme: Theme) =>
createStyles({
logList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
}
});

interface ITrace {
classes: any;
traceMessageReceived: typeof traceMessageReceived;
traceResetMessages: typeof traceResetMessages;
messages: TraceMessage[];
}

const Trace = ({
classes,
traceMessageReceived,
traceResetMessages,
messages
}: ITrace) => {
useEffect(() => {
traceResetMessages();
const token: string = storage.getItem("token")!;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;

const setCookie = (name: string, val: string) => {
const date = new Date();
const value = val;

// Set it expire in 45 minutes
date.setTime(date.getTime() + 45 * 60 * 1000);

// Set it
document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
};

setCookie("token", token);

const c = new W3CWebSocket(`ws://${url.hostname}:${port}/ws/trace`);

let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
let m: TraceMessage = JSON.parse(message.data.toString());
m.time = new Date(m.time.toString());
m.key = Math.random();
traceMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
}, [traceMessageReceived]);

const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;

while (n >= 1024 && ++l) {
n = n / 1024;
}
//include a decimal point and a tenths-place digit if presenting
//less than ten of KB or greater units
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
};
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;

return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};

return (
<div>
<h1>Trace</h1>
<div className={classes.logList}>
<ul>
{messages.map(m => {
return (
<li key={m.key}>
{timeFromdate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
{m.api} {m.host} {m.client} {m.callStats.duration}{" "}
{niceBytes(m.callStats.rx + "")}{" "}
{niceBytes(m.callStats.tx + "")}
</li>
);
})}
</ul>
</div>
</div>
);
};

const mapState = (state: AppState) => ({
messages: state.trace.messages
});

const connector = connect(mapState, {
traceMessageReceived: traceMessageReceived,
traceResetMessages: traceResetMessages
});

export default connector(withStyles(styles)(Trace));
46 changes: 46 additions & 0 deletions portal-ui/src/screens/Console/Trace/actions.ts
@@ -0,0 +1,46 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 { TraceMessage } from "./types";

export const TRACE_MESSAGE_RECEIVED = "TRACE_MESSAGE_RECEIVED";
export const TRACE_RESET_MESSAGES = "TRACE_RESET_MESSAGES";

interface TraceMessageReceivedAction {
type: typeof TRACE_MESSAGE_RECEIVED;
message: TraceMessage;
}

interface TraceResetMessagesAction {
type: typeof TRACE_RESET_MESSAGES;
}

export type TraceActionTypes =
| TraceMessageReceivedAction
| TraceResetMessagesAction;

export function traceMessageReceived(message: TraceMessage) {
return {
type: TRACE_MESSAGE_RECEIVED,
message: message
};
}

export function traceResetMessages() {
return {
type: TRACE_RESET_MESSAGES
};
}
50 changes: 50 additions & 0 deletions portal-ui/src/screens/Console/Trace/reducers.ts
@@ -0,0 +1,50 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 {
TRACE_MESSAGE_RECEIVED,
TRACE_RESET_MESSAGES,
TraceActionTypes
} from "./actions";
import { TraceMessage } from "./types";

export interface TraceState {
messages: TraceMessage[];
}

const initialState: TraceState = {
messages: []
};

export function traceReducer(
state = initialState,
action: TraceActionTypes
): TraceState {
switch (action.type) {
case TRACE_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case TRACE_RESET_MESSAGES:
return {
...state,
messages: []
};
default:
return state;
}
}
35 changes: 35 additions & 0 deletions portal-ui/src/screens/Console/Trace/types.ts
@@ -0,0 +1,35 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.

export interface CallStats {
timeToFirstByte: string;
rx: number;
tx: number;
duration: string;
}

export interface TraceMessage {
client: string;
time: Date;
statusCode: number;
api: string;
query: string;
host: string;
callStats: CallStats;
path: string;
statusMsg: string;
key: number;
}
4 changes: 3 additions & 1 deletion portal-ui/src/store.ts
Expand Up @@ -17,9 +17,11 @@
import { applyMiddleware, combineReducers, compose, createStore } from "redux";
import thunk from "redux-thunk";
import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers";

const globalReducer = combineReducers({
system: systemReducer
system: systemReducer,
trace: traceReducer
});

declare global {
Expand Down

0 comments on commit fe1acaa

Please sign in to comment.