diff --git a/src/common/LogCard.tsx b/src/common/LogCard.tsx new file mode 100644 index 0000000..8e54b1e --- /dev/null +++ b/src/common/LogCard.tsx @@ -0,0 +1,133 @@ +import { + Box, + Drawer, + Grid2 as Grid, + Theme, + Typography, + useTheme +} from "@mui/material"; +import { grey, lightBlue, red, orange } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import React, { useState } from "react"; + +interface Props { + index: number; + datalog: pond.Log; +} + +const useStyles = makeStyles((theme: Theme) => ({ + logbox: { + width: "100%", + padding: theme.spacing(1), + "&:hover": { + filter: "brightness(1.15)", + cursor: "pointer" + } + } + }) +); + +export default function LogCard(props: Props) { + const { datalog, index } = props; + let log = datalog; + let attributes = JSON.parse(log.attributes); + // let elementKey = event.target.getAttribute('a-key'); + const classes = useStyles(); + const theme = useTheme(); + + const [drawer, setDrawer] = useState(false); + + if (log.level === undefined) log.level = "info"; + + let barColor: string = lightBlue[400]; + if (log.level.toLowerCase() === "debug") { + barColor = grey[400]; + } else if (log.level.toLowerCase() === "warning") { + barColor = orange[500]; + } else if (log.level.toLowerCase() === "error") { + barColor = red[500]; + } else if (log.level.toLowerCase() === "critical") { + barColor = red["A100"]; + } + let bc = index % 2 === 0 ? theme.palette.background.default : theme.palette.background.paper; + + const lineSummary = () => { + return ( + + + {moment(log.timestamp).format("MMM Do, h:mm:ss a")} + + + {log.service} + + + {log.device} + + + {log.message} + + + + {log.attributes} + + + + ); + }; + + return ( + + setDrawer(false)} anchor="right"> + + + + + Device: + + + {log.device} + + + + + Timestamp: + + + {moment(log.timestamp).format("MMMM ddd Do, h:mm:ss a")} + + + + + Level: + + + {log.level} + + + + + Service: + + + {log.service} + + + + { + + {JSON.stringify(attributes, null, 2)} + + } + + + setDrawer(true)}> + {lineSummary()} + + + ); +} diff --git a/src/common/LogsDisplay.tsx b/src/common/LogsDisplay.tsx new file mode 100644 index 0000000..07f712d --- /dev/null +++ b/src/common/LogsDisplay.tsx @@ -0,0 +1,609 @@ +import { + Box, + Button, + Checkbox, + CircularProgress, + FormControlLabel, + FormGroup, + Grid, + IconButton, + List, + ListItem, + MenuItem, + TextField, + Theme, + Typography, + useTheme +} from "@mui/material"; +import { pond } from "protobuf-ts/pond"; +import { useEffect, useState } from "react"; +import LogCard from "./LogCard"; +import moment from "moment"; +import { useDataDogProxyAPI } from "providers"; +import { Cell, Legend, Pie, PieChart, ResponsiveContainer } from "recharts"; +import { grey, lightBlue, orange, red } from "@mui/material/colors"; +import SearchIcon from "@mui/icons-material/Search"; +import { makeStyles } from "@mui/styles"; + +interface DataAmount { + dataAmount: number; + name: string; + color: string; +} + +interface Props { + deviceID?: number; +} + +const levelOptions = [ + { + key: "All", + val: "all" + }, + { + key: "Info", + val: "info" + }, + { + key: "Debug", + val: "debug" + }, + { + key: "Warning", + val: "warning" + }, + { + key: "Error", + val: "error" + }, + { + key: "Critical", + val: "critical" + } +]; + +const sortOptions = [ + { + key: "Ascending", + val: "asc" + }, + { + key: "Descending", + val: "desc" + } +]; + +const useStyles = makeStyles((theme: Theme) => ({ + sidebox: { + borderRadius: 5, + borderColor: theme.palette.text.primary, + border: "1px solid", + padding: theme.spacing(1), + marginRight: theme.spacing(2) + } + }) +); + +export default function LogsDisplay(props: Props) { + const { deviceID } = props; + + const theme = useTheme(); + const classes = useStyles(); + + const [sort, setSort] = useState("desc"); + const [limit, setLimit] = useState(50); + const [level, setLevel] = useState("all"); + const [service, setService] = useState("all"); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [device, setDevice] = useState(0); + + const [usage, setUsage] = useState([]); + const [customQuery, setCustomQuery] = useState(""); + // const [pieLabel, setPieLabel] = useState(""); + // const [pieData, setPieData] = useState(""); + + const DDAPI = useDataDogProxyAPI(); + + const [from, setFrom] = useState( + moment() + .subtract(1, "day") + .format("YYYY-MM-DD") + ); + const [to, setTo] = useState( + moment() + .add("2", "days") + .format("YYYY-MM-DD") + ); + + const addToQuery = (query: string, input: string) => { + if (query.length > 0) query = query + " AND "; + query = query + input; + return query; + }; + + const getLogs = () => { + setLoading(true); + let query = ""; + if (level !== "all") + query = addToQuery(query, "@level:(" + level + " OR " + level.toUpperCase() + ")"); + if (deviceID) { + query = addToQuery(query, "@device:" + deviceID); + } else if (device > 0) { + query = addToQuery(query, "@device:" + device); + } + if (service !== "all") query = addToQuery(query, "service:" + service); + setCustomQuery(query); + DDAPI.listLogs(from, to, sort, limit, query) + .then(resp => { + setLogs(resp.data.logs); + }) + .finally(() => { + setLoading(false); + }); + }; + + const getCustomQuery = () => { + setLoading(true); + DDAPI.listLogs(from, to, sort, limit, customQuery) + .then(resp => { + setLogs(resp.data.logs); + }) + .finally(() => { + setLoading(false); + }); + }; + + const titleGrid = () => { + return ( + + + setFrom(e.target.value)} + /> + + + setTo(e.target.value)} + /> + + + setLevel(event.target.value)}> + {levelOptions.map(op => ( + + {op.key} + + ))} + + + + setLimit(+e.target.value)} + /> + + + setSort(event.target.value)}> + {sortOptions.map(op => ( + + {op.key} + + ))} + + + {!deviceID && ( + + setDevice(parseInt(event.target.value))}> + {sortOptions.map(op => ( + + {op.key} + + ))} + + + )} + + {loading ? ( + + ) : ( + + Get Logs + + )} + + + ); + }; + + const endAdornment = () => { + // if (loading) return + return ( + + + + ); + }; + + const searchBar = () => { + return ( + + + setCustomQuery(e.target.value)} + onKeyPress={e => { + if (e.key === "Enter" && !loading) { + getCustomQuery(); + } + }} + /> + + + ); + }; + + useEffect(() => { + if (logs.length < 1) { + let data = { dataAmount: 100, name: "Dummy", color: theme.palette.background.paper }; + setUsage([data]); + return; + } + + let debugData = { dataAmount: 0, name: "Debug", color: grey[500] }; + let infoData = { dataAmount: 0, name: "Info", color: lightBlue[200] }; + let warningData = { dataAmount: 0, name: "Warning", color: orange[500] }; + let errorData = { dataAmount: 0, name: "Error", color: red[500] }; + let criticalData = { dataAmount: 0, name: "Critical", color: red["A100"] }; + logs.forEach(log => { + switch (log.level.toLowerCase()) { + case "debug": + debugData.dataAmount++; + break; + case "info": + infoData.dataAmount++; + break; + case "warning": + warningData.dataAmount++; + break; + case "error": + errorData.dataAmount++; + break; + case "critical": + criticalData.dataAmount++; + break; + } + }); + setUsage([debugData, infoData, warningData, errorData, criticalData]); + }, [logs, theme.palette.background.paper]); + + const logPieChart = () => { + return ( + + + + ( + + {/* + hi + */} + {logs.length < 1 && No Logs} + {/* {device.status.bytesTimestamp.length > 1 && ( + {pieData} + )} */} + + )} + /> + + {usage.map((entry, index) => ( + { + // setPieLabel(entry.name); + // setPieData(entry.dataAmount.toString()); + }} + /> + ))} + + + + + ); + }; + + // const check = (label: string) => { + // if (service === label) { + // setService("all"); + // } else { + // setService(label); + // } + // }; + + const serviceSelector = () => { + return ( + + Service + + setService("all")} + /> + } + label="all" + labelPlacement="end" + /> + setService("pond")} + /> + } + label="pond" + labelPlacement="end" + /> + setService("quay")} + /> + } + label="quay" + labelPlacement="end" + /> + setService("traefik")} + /> + } + label="traefik" + labelPlacement="end" + /> + setService("webui")} + /> + } + label="webui" + labelPlacement="end" + /> + setService("agent")} + /> + } + label="agent" + labelPlacement="end" + /> + + + ); + }; + + const getPercent = (dataNum: number) => { + if (logs.length < 1 || logs.length < dataNum) return 0 + "%"; + let newNum = (dataNum / logs.length) * 100; + return newNum.toFixed(0) + "%"; + }; + + const logLevels = () => { + let debugNum = 0; + let infoNum = 0; + let warningNum = 0; + let errorNum = 0; + let criticalNum = 0; + usage.forEach(data => { + switch (data.name) { + case "Debug": + debugNum = data.dataAmount; + break; + case "Info": + infoNum = data.dataAmount; + break; + case "Warning": + warningNum = data.dataAmount; + break; + case "Error": + errorNum = data.dataAmount; + break; + case "Critical": + criticalNum = data.dataAmount; + break; + } + }); + return ( + + + + ■ + + Info: {infoNum} + + + ({getPercent(infoNum)}) + + + + ■ + + Debug: {debugNum} + + + ({getPercent(debugNum)}) + + + + ■ + + Warning: {warningNum} + + + ({getPercent(warningNum)}) + + + + ■ + + Error: {errorNum} + + + ({getPercent(errorNum)}) + + + + ■ + + Critical: {criticalNum} + + + ({getPercent(criticalNum)}) + + + + + ); + }; + + return ( + + {/* Title Section */} + + {titleGrid()} + {searchBar()} + + + {/* Header Section */} + + + Log Metrics + {/* Pie chart and service selector will go here */} + {logPieChart()} + {logLevels()} + {/* */} + {serviceSelector()} + + + + + Timestamp + + + Service + + + Device + + + Message + + + Content + + + + {/* Scrollable List Section */} + + {logs.map((l, i) => ( + + ))} + {logs.length < 1 && ( + + Found logs will be displayed here + + )} + + + + + ); +} diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 9fc066a..3172b54 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -37,6 +37,7 @@ const BinCableEstimator = lazy(() => import("pages/BinCableEstimator")); const APIDocs = lazy(() => import("pages/APIDocs")); const Fields = lazy(()=> import("pages/Fields")); const Marketplace = lazy(() => import("userFeatures/UserFeatures")) +const Logs = lazy(() => import("pages/Logs")) interface Props { toggleTheme: () => void; @@ -314,6 +315,9 @@ export default function Router(props: Props) { {user.hasFeature("developer") && } /> } + {user.hasFeature("admin") && + } /> + } } /> } /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 0a2cc9d..2730a9f 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -43,6 +43,7 @@ import TasksIcon from "products/Construction/TasksIcon"; import CableIcon from "products/Bindapt/CableIcon"; import FieldListIcon from "products/AgIcons/FieldList"; import MarketplaceIcon from "products/CommonIcons/marketplaceIcon"; +import DataDuckIcon from "products/Bindapt/DataDuckIcon"; const drawerWidth = 230; @@ -367,6 +368,20 @@ export default function SideNavigator(props: Props) { } + {user.hasFeature("admin") && + + goTo("/logs")} + classes={getClasses("/logs")} + > + + + + {open && } + + + } + + + + + ); +} \ No newline at end of file diff --git a/src/providers/index.ts b/src/providers/index.ts index 25aebe4..6282227 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -33,7 +33,7 @@ export { // useUsageAPI, useUserAPI, // useStripeAPI, - // useDataDogProxyAPI, + useDataDogProxyAPI, useMineAPI, // useKeyManagerAPI, useTerminalAPI, //TODO: update api with resolve,reject diff --git a/src/providers/pond/datadogProxyAPI.tsx b/src/providers/pond/datadogProxyAPI.tsx new file mode 100644 index 0000000..ec957a4 --- /dev/null +++ b/src/providers/pond/datadogProxyAPI.tsx @@ -0,0 +1,107 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; + +export interface IDataDogProxyAPIContext { + listLogs: ( + from: string, + to: string, + sort: string, + limit: number, + level?: string + ) => Promise>; + listDeviceLogs: ( + device: string | number, + from: string, + to: string, + sort: string, + limit: number, + level?: string + ) => Promise>; +} + +export const DataDogProxyAPIContext = createContext( + {} as IDataDogProxyAPIContext +); + +interface Props {} + +export default function DataDogProvider(props: PropsWithChildren) { + const { children } = props; + const { get } = useHTTP(); + + const listLogs = (from: string, to: string, sort: string, limit: number, query?: string) => { + query = query ? query : ""; + // if (level && level !== "all") { + // query = query + " @level:" + level; + // } + return get( + pondURL( + "/datadog/logs?query=" + + query + + "&from=" + + from + + "&to=" + + to + + "&sort=" + + sort + + "&limit=" + + limit + ) + ); + }; + + const listDeviceLogs = ( + device: string | number, + from: string, + to: string, + sort: string, + limit: number, + level?: string + ) => { + let query = "@device:" + device; + if (level && level !== "all") { + query = query + " @level:" + level; + } + // console.log( + // "/datadog/logs?query=" + + // query + + // "&from=" + + // from + + // "&to=" + + // to + + // "&sort=" + + // sort + + // "&limit=" + + // limit + // ); + return get( + pondURL( + "/datadog/logs?query=" + + query + + "&from=" + + from + + "&to=" + + to + + "&sort=" + + sort + + "&limit=" + + limit + ) + ); + }; + + return ( + + {children} + + ); +} + +export const useDataDogProxyAPI = () => useContext(DataDogProxyAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index b6212e7..aa4aa1a 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -30,6 +30,7 @@ import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI"; import MutationProvider, {useMutationAPI} from "./mutationAPI" import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI"; import TaskProvider, { useTaskAPI } from "./taskAPI"; +import DataDogProvider, { useDataDogProxyAPI } from "./datadogProxyAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -76,7 +77,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -139,5 +142,6 @@ export { useObjectHeaterAPI, useMutationAPI, usePreferenceAPI, - useTaskAPI + useTaskAPI, + useDataDogProxyAPI };
{JSON.stringify(attributes, null, 2)}