continue work on LogItem

This commit is contained in:
Sidney Alcantara
2021-11-04 15:26:14 +11:00
parent 528a7e9f7a
commit f638663d1d
8 changed files with 365 additions and 41 deletions

View File

@@ -32,6 +32,7 @@
"file-saver": "^2.0.5",
"firebase": "8.6.8",
"hotkeys-js": "^3.7.2",
"jotai": "^1.4.2",
"json-format": "^1.0.1",
"json2csv": "^5.0.6",
"jszip": "^3.6.0",

View File

@@ -0,0 +1,94 @@
import useSWR from "swr";
import _get from "lodash/get";
import Modal, { IModalProps } from "@src/components/Modal";
import { List, ListSubheader } from "@mui/material";
import LogItem from "./LogItem";
import { useProjectContext } from "@src/contexts/ProjectContext";
export default function CloudLogsModal(props: IModalProps) {
const { rowyRun } = useProjectContext();
const { data: logItems } = useSWR(
"logItems",
() =>
rowyRun
? rowyRun<Record<string, any>[]>({
route: {
// path: "/logs",
// path: '/logs?filter=resource.labels.function_name="R-githubStars"',
path: `/logs?filter=logName="${encodeURIComponent(
"projects/rowyio/logs/rowy-audit-logs"
)}"`,
method: "GET",
},
})
: [],
{ fallbackData: [], revalidateOnMount: true }
);
const renderedLogItems: React.ReactNodeArray = [];
if (Array.isArray(logItems)) {
for (let i = 0; i < logItems.length; i++) {
const logItem = logItems[i];
const prevItem = logItems[i - 1];
if (
_get(logItem, "labels.execution_id") !==
_get(prevItem, "labels.execution_id")
) {
renderedLogItems.push(
<ListSubheader
key={_get(logItem, "labels.execution_id")}
disableGutters
disableSticky
sx={{
mt: 2,
typography: "subtitle2",
py: (32 - 20) / 2 / 8,
"& code": { fontSize: "90%" },
}}
>
Function{" "}
<code>{_get(logItem, "resource.labels.function_name")}</code>{" "}
execution <code>{_get(logItem, "labels.execution_id")}</code>
</ListSubheader>
);
}
renderedLogItems.push(
<li key={logItem.insertId}>
<LogItem
data={logItem}
chips={[
// Rowy Run HTTP request
"httpRequest.requestMethod",
"httpRequest.status",
// Rowy audit logs
"jsonPayload.eventType",
"jsonPayload.eventData.rowPath",
"jsonPayload.eventData.updatedField",
"jsonPayload.fields.rowyUser.structValue.fields.displayName.stringValue",
]}
/>
</li>
);
}
}
return (
<Modal
{...props}
maxWidth="xl"
fullWidth
fullHeight
title={`Cloud logs (${logItems?.length})`}
>
<List component="ol" subheader={<li />}>
{renderedLogItems}
</List>
</Modal>
);
}

View File

@@ -1,43 +1,169 @@
import { format } from "date-fns";
import _get from "lodash/get";
import ReactJson from "react-json-view";
import {
styled,
useTheme,
Accordion as MuiAccordion,
AccordionProps as MuiAccordionProps,
AccordionSummary,
AccordionDetails,
AccordionSummary as MuiAccordionSummary,
AccordionDetails as MuiAccordionDetails,
Stack,
Chip as MuiChip,
ChipProps,
Typography,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import LogSeverityIcon from "./LogSeverityIcon";
const Accordion = styled((props: MuiAccordionProps) => (
<MuiAccordion disableGutters elevation={0} square {...props} />
))({
import { DATE_FORMAT, TIME_FORMAT } from "@src/constants/dates";
const Accordion = styled(MuiAccordion)(({ theme }) => ({
marginTop: 0,
"&.Mui-expanded:before": { opacity: 1 },
"&::before": { display: "none" },
...theme.typography.caption,
fontFamily: theme.typography.fontFamilyMono,
}));
const AccordionSummary = styled(MuiAccordionSummary)(({ theme }) => ({
minHeight: 32,
alignItems: "flex-start",
"& svg": {
fontSize: 18,
height: 20,
},
"& .MuiAccordionSummary-content, & .MuiAccordionSummary-expandIconWrapper": {
marginTop: (32 - 20) / 2,
marginBottom: (32 - 20) / 2,
},
"& .MuiAccordionSummary-content": {
overflow: "hidden",
paddingRight: theme.spacing(1),
display: "flex",
alignItems: "flex-start",
gap: theme.spacing(0.5, 2),
"& > *": { flexShrink: 0 },
[theme.breakpoints.down("lg")]: {
flexWrap: "wrap",
paddingLeft: theme.spacing(18 / 8 + 2),
"& > :first-child": { marginLeft: theme.spacing((18 / 8 + 2) * -1) },
},
},
"& .log-preview": {
flexShrink: 1,
".Mui-expanded&": {
overflow: "visible",
whiteSpace: "pre-wrap",
},
},
margin: theme.spacing(0, -1.5),
padding: theme.spacing(0, 1.375, 0, 1.5),
borderRadius: theme.shape.borderRadius,
"&:hover": { backgroundColor: theme.palette.action.hover },
userSelect: "auto",
}));
const Chip = styled((props: ChipProps) => <MuiChip size="small" {...props} />)({
font: "inherit",
minHeight: 20,
padding: 0,
cursor: "inherit",
});
export interface ILogItemProps {}
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
paddingLeft: theme.spacing(18 / 8 + 2),
paddingRight: 0,
}));
export interface ILogItemProps {
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.insert_id
data: Record<string, any>;
chips?: string[];
}
export default function LogItem({ data, chips }: ILogItemProps) {
const theme = useTheme();
const timestamp = new Date(
data.timestamp.seconds * 1000 + data.timestamp.nanos / 1_000_000
);
const renderedChips = Array.isArray(chips)
? chips
.map((key) => {
const value = _get(data, key);
if (!value) return null;
return (
<Chip
key={key}
label={
typeof value === "string" || typeof value === "number"
? value
: JSON.stringify(value)
}
aria-describedby={key}
/>
);
})
.filter(Boolean)
: [];
export default function LogItem(props: ILogItemProps) {
return (
<Accordion>
<Accordion
disableGutters
elevation={0}
square
TransitionProps={{ unmountOnExit: true }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
style={{ padding: 0 }}
aria-controls={`${data.insertId}-content`}
id={`${data.insertId}-header`}
>
<Typography sx={{ width: "33%", flexShrink: 0 }}>
General settings
</Typography>
<Typography sx={{ color: "text.secondary" }}>
I am an accordion
<LogSeverityIcon severity={data.severity} />
<time dateTime={timestamp.toISOString()}>
<Typography variant="inherit" color="text.secondary" component="span">
{format(timestamp, DATE_FORMAT)}
</Typography>{" "}
<Typography variant="inherit" fontWeight="bold" component="span">
{format(timestamp, TIME_FORMAT)}
</Typography>
<Typography variant="inherit" color="text.secondary" component="span">
{format(timestamp, ":ss.SSS")}
</Typography>
</time>
{renderedChips.length > 0 && (
<Stack direction="row" spacing={0.75}>
{renderedChips}
</Stack>
)}
<Typography variant="inherit" noWrap className="log-preview">
{data.payload === "textPayload" && data.textPayload}
{_get(data, "httpRequest.requestUrl")?.split(".run.app").pop()}
{data.payload === "jsonPayload" && JSON.stringify(data.jsonPayload)}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>
Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat.
Aliquam eget maximus est, id dignissim quam.
</Typography>
<ReactJson
src={data}
theme={theme.palette.mode === "dark" ? "monokai" : "rjv-default"}
iconStyle="triangle"
style={{ font: "inherit", backgroundColor: "transparent" }}
/>
</AccordionDetails>
</Accordion>
);

View File

@@ -0,0 +1,100 @@
import { SvgIcon, SvgIconProps, Tooltip } from "@mui/material";
import DebugIcon from "@mui/icons-material/BugReportOutlined";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import NoticeIcon from "@mui/icons-material/NotificationsOutlined";
import WarningIcon from "@mui/icons-material/WarningAmberOutlined";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import { mdiCarBrakeAlert } from "@mdi/js";
import AlertIcon from "@mui/icons-material/Error";
import EmergencyIcon from "@mui/icons-material/NewReleases";
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
export const SEVERITY_LEVELS = {
DEFAULT: "The log entry has no assigned severity level.",
DEBUG: "Debug or trace information.",
INFO: "Routine information, such as ongoing status or performance.",
NOTICE:
"Normal but significant events, such as start up, shut down, or a configuration change.",
WARNING: "Warning events might cause problems.",
ERROR: "Error events are likely to cause problems.",
CRITICAL: "Critical events cause more severe problems or outages.",
ALERT: "A person must take an action immediately.",
EMERGENCY: "One or more systems are unusable.",
};
export interface ILogSeverityIconProps extends SvgIconProps {
severity: keyof typeof SEVERITY_LEVELS;
}
export default function LogSeverityIcon({
severity,
...props
}: ILogSeverityIconProps) {
const commonIconProps: SvgIconProps = {
...props,
"aria-hidden": "false",
"aria-label": `Severity: ${severity.toLowerCase()}`,
};
let icon = (
<SvgIcon {...commonIconProps} color="disabled" viewBox="0 0 18 18">
<circle cx="9" cy="9" r="2" />
</SvgIcon>
);
switch (severity) {
case "DEBUG":
icon = <DebugIcon {...commonIconProps} color="action" />;
break;
case "INFO":
icon = <InfoIcon {...commonIconProps} color="info" />;
break;
case "NOTICE":
icon = <NoticeIcon {...commonIconProps} color="info" />;
break;
case "WARNING":
icon = <WarningIcon {...commonIconProps} color="warning" />;
break;
case "ERROR":
icon = <ErrorIcon {...commonIconProps} color="error" />;
break;
case "CRITICAL":
icon = (
<SvgIcon {...commonIconProps} color="error">
<path d={mdiCarBrakeAlert} />
</SvgIcon>
);
break;
case "ALERT":
icon = <AlertIcon {...commonIconProps} color="error" />;
break;
case "EMERGENCY":
icon = <EmergencyIcon {...commonIconProps} color="error" />;
break;
default:
break;
}
return (
<Tooltip
title={
<>
{severity}
<br />
{SEVERITY_LEVELS[severity]}
</>
}
describeChild
>
{icon}
</Tooltip>
);
}

View File

@@ -1,15 +1,21 @@
import { useState } from "react";
import LogsIcon from "@src/assets/icons/CloudLogs";
import { useAtom } from "jotai";
import { atomWithHash } from "jotai/utils";
import TableHeaderButton from "../TableHeaderButton";
import Modal from "@src/components/Modal";
import LogItem from "./LogItem";
import LogsIcon from "@src/assets/icons/CloudLogs";
import CloudLogsModal from "./CloudLogsModal";
const modalAtom = atomWithHash("modal", "");
// const modalStateAtom = atomWithHash<Record<string, any>>("modalState", {});
export interface ICloudLogsProps {}
export default function CloudLogs(props: ICloudLogsProps) {
const [open, setOpen] = useState(false);
const [modal, setModal] = useAtom(modalAtom);
const open = modal === "cloudLogs";
const setOpen = (open: boolean) => setModal(open ? "cloudLogs" : "");
// const [modalState, setModalState] = useAtom(modalStateAtom);
return (
<>
@@ -20,16 +26,7 @@ export default function CloudLogs(props: ICloudLogsProps) {
/>
{open && (
<Modal
onClose={() => setOpen(false)}
maxWidth="xl"
fullWidth
fullHeight
title="Cloud logs"
>
<LogItem />
<LogItem />
</Modal>
<CloudLogsModal onClose={() => setOpen(false)} title="Cloud logs" />
)}
</>
);

View File

@@ -1,2 +1,3 @@
export const DATE_FORMAT = "yyyy-MM-dd";
export const DATE_TIME_FORMAT = DATE_FORMAT + " HH:mm";
export const TIME_FORMAT = "HH:mm";
export const DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;

View File

@@ -80,9 +80,9 @@ interface IProjectContext {
// A ref ot the import wizard. Prevents unnecessary re-renders
importWizardRef: React.MutableRefObject<ImportWizardRef | undefined>;
rowyRun: (
rowyRun: <T = any>(
args: Omit<IRowyRunRequestProps, "rowyRunUrl" | "authToken">
) => Promise<any>;
) => Promise<T>;
}
const ProjectContext = React.createContext<Partial<IProjectContext>>({});

View File

@@ -9898,6 +9898,11 @@ join-path@^1.1.1:
url-join "0.0.1"
valid-url "^1"
jotai@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.4.2.tgz#0747581840c82ec0862d4c15ee0f7d59246ed46e"
integrity sha512-/NcK8DGvfGcVCqoOvjWIo8/KaUYtadXEl+6uxLiQJUxbyiqCtXkhAdrugk5jmpAFXXD2y6fNDw2Ln7h0EuY+ng==
jpeg-js@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b"