mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
continue work on LogItem
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
100
src/components/Table/TableHeader/CloudLogs/LogSeverityIcon.tsx
Normal file
100
src/components/Table/TableHeader/CloudLogs/LogSeverityIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>({});
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user