mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
finish CloudLogItem
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"notistack": "^2.0.2",
|
||||
"pb-util": "^1.0.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { format } from "date-fns";
|
||||
import _get from "lodash/get";
|
||||
import ReactJson from "react-json-view";
|
||||
import { struct } from "pb-util";
|
||||
import jsonFormat from "json-format";
|
||||
|
||||
import {
|
||||
styled,
|
||||
@@ -12,13 +14,15 @@ import {
|
||||
Chip as MuiChip,
|
||||
ChipProps,
|
||||
Typography,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import LogSeverityIcon from "./LogSeverityIcon";
|
||||
import CloudLogSeverityIcon from "./CloudLogSeverityIcon";
|
||||
|
||||
import { DATE_FORMAT, TIME_FORMAT } from "@src/constants/dates";
|
||||
|
||||
const Accordion = styled(MuiAccordion)(({ theme }) => ({
|
||||
background: "none",
|
||||
marginTop: 0,
|
||||
"&::before": { display: "none" },
|
||||
|
||||
@@ -30,6 +34,11 @@ const AccordionSummary = styled(MuiAccordionSummary)(({ theme }) => ({
|
||||
minHeight: 32,
|
||||
alignItems: "flex-start",
|
||||
|
||||
"&.Mui-expanded": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
"&:hover": { backgroundColor: theme.palette.action.selected },
|
||||
},
|
||||
|
||||
"& svg": {
|
||||
fontSize: 18,
|
||||
height: 20,
|
||||
@@ -58,13 +67,12 @@ const AccordionSummary = styled(MuiAccordionSummary)(({ theme }) => ({
|
||||
"& .log-preview": {
|
||||
flexShrink: 1,
|
||||
|
||||
".Mui-expanded&": {
|
||||
overflow: "visible",
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
// ".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 },
|
||||
@@ -80,19 +88,26 @@ const Chip = styled((props: ChipProps) => <MuiChip size="small" {...props} />)({
|
||||
});
|
||||
|
||||
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(18 / 8 + 2),
|
||||
paddingRight: 0,
|
||||
paddingLeft: theme.spacing(18 / 8 + 2 + 1.5),
|
||||
paddingRight: theme.spacing(18 / 8 + 2 + 1.5),
|
||||
}));
|
||||
|
||||
export interface ILogItemProps {
|
||||
export interface ICloudLogItemProps {
|
||||
// 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) {
|
||||
export default function CloudLogItem({
|
||||
data: dataProp,
|
||||
chips,
|
||||
}: ICloudLogItemProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const data = { ...dataProp };
|
||||
if (dataProp.payload === "jsonPayload" && dataProp.jsonPayload)
|
||||
data.jsonPayload = struct.decode(dataProp.jsonPayload ?? {});
|
||||
|
||||
const timestamp = new Date(
|
||||
data.timestamp.seconds * 1000 + data.timestamp.nanos / 1_000_000
|
||||
);
|
||||
@@ -130,7 +145,7 @@ export default function LogItem({ data, chips }: ILogItemProps) {
|
||||
aria-controls={`${data.insertId}-content`}
|
||||
id={`${data.insertId}-header`}
|
||||
>
|
||||
<LogSeverityIcon severity={data.severity} />
|
||||
<CloudLogSeverityIcon severity={data.severity} />
|
||||
|
||||
<time dateTime={timestamp.toISOString()}>
|
||||
<Typography variant="inherit" color="text.secondary" component="span">
|
||||
@@ -153,16 +168,40 @@ export default function LogItem({ data, chips }: ILogItemProps) {
|
||||
<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)}
|
||||
{data.payload === "jsonPayload" &&
|
||||
jsonFormat(data.jsonPayload, { type: "space", char: " ", size: 2 })}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails>
|
||||
{data.payload === "textPayload" && (
|
||||
<Typography variant="inherit" style={{ whiteSpace: "pre-wrap" }}>
|
||||
{data.textPayload}
|
||||
</Typography>
|
||||
)}
|
||||
{data.payload === "jsonPayload" && (
|
||||
<ReactJson
|
||||
src={data.jsonPayload}
|
||||
name="jsonPayload"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "rjv-default"}
|
||||
iconStyle="triangle"
|
||||
style={{ font: "inherit", backgroundColor: "transparent" }}
|
||||
displayDataTypes={false}
|
||||
quotesOnKeys={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.payload && <Divider sx={{ my: 1 }} />}
|
||||
|
||||
<ReactJson
|
||||
src={data}
|
||||
collapsed={!!data.payload}
|
||||
name="Full log entry"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "rjv-default"}
|
||||
iconStyle="triangle"
|
||||
style={{ font: "inherit", backgroundColor: "transparent" }}
|
||||
displayDataTypes={false}
|
||||
quotesOnKeys={false}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
56
src/components/Table/TableHeader/CloudLogs/CloudLogList.tsx
Normal file
56
src/components/Table/TableHeader/CloudLogs/CloudLogList.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import _get from "lodash/get";
|
||||
|
||||
import { List, ListProps } from "@mui/material";
|
||||
import CloudLogSubheader from "./CloudLogSubheader";
|
||||
import CloudLogItem from "./CloudLogItem";
|
||||
|
||||
export interface ICloudLogListProps extends Partial<ListProps> {
|
||||
items: Record<string, any>[];
|
||||
}
|
||||
|
||||
export default function CloudLogList({ items, ...props }: ICloudLogListProps) {
|
||||
const renderedLogItems: React.ReactNodeArray = [];
|
||||
if (Array.isArray(items)) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const prevItem = items[i - 1];
|
||||
|
||||
if (
|
||||
_get(item, "labels.execution_id") !==
|
||||
_get(prevItem, "labels.execution_id")
|
||||
) {
|
||||
renderedLogItems.push(
|
||||
<CloudLogSubheader key={_get(item, "labels.execution_id")}>
|
||||
Function <code>{_get(item, "resource.labels.function_name")}</code>{" "}
|
||||
execution <code>{_get(item, "labels.execution_id")}</code>
|
||||
</CloudLogSubheader>
|
||||
);
|
||||
}
|
||||
|
||||
renderedLogItems.push(
|
||||
<li key={item.insertId}>
|
||||
<CloudLogItem
|
||||
data={item}
|
||||
chips={[
|
||||
// Rowy Run HTTP request
|
||||
"httpRequest.requestMethod",
|
||||
"httpRequest.status",
|
||||
// Rowy audit logs
|
||||
"jsonPayload.type",
|
||||
"jsonPayload.ref.tableId",
|
||||
"jsonPayload.ref.rowId",
|
||||
"jsonPayload.data.updatedField",
|
||||
"jsonPayload.rowyUser.displayName",
|
||||
]}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<List disablePadding {...({ component: "ol" } as any)} {...props}>
|
||||
{renderedLogItems}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -22,14 +22,14 @@ export const SEVERITY_LEVELS = {
|
||||
EMERGENCY: "One or more systems are unusable.",
|
||||
};
|
||||
|
||||
export interface ILogSeverityIconProps extends SvgIconProps {
|
||||
export interface ICloudLogSeverityIconProps extends SvgIconProps {
|
||||
severity: keyof typeof SEVERITY_LEVELS;
|
||||
}
|
||||
|
||||
export default function LogSeverityIcon({
|
||||
export default function CloudLogSeverityIcon({
|
||||
severity,
|
||||
...props
|
||||
}: ILogSeverityIconProps) {
|
||||
}: ICloudLogSeverityIconProps) {
|
||||
const commonIconProps: SvgIconProps = {
|
||||
...props,
|
||||
"aria-hidden": "false",
|
||||
@@ -0,0 +1,18 @@
|
||||
import { styled, ListSubheader } from "@mui/material";
|
||||
|
||||
export const CloudLogSubheader = styled((props) => (
|
||||
<ListSubheader disableGutters disableSticky={false} {...props} />
|
||||
))(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
...theme.typography.subtitle2,
|
||||
padding: theme.spacing((32 - 20) / 2 / 8, 1.5),
|
||||
|
||||
"& code": { fontSize: "90%" },
|
||||
|
||||
".MuiPaper-elevation24 &": {
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))",
|
||||
},
|
||||
}));
|
||||
|
||||
export default CloudLogSubheader;
|
||||
@@ -1,16 +1,17 @@
|
||||
import useSWR from "swr";
|
||||
import _get from "lodash/get";
|
||||
|
||||
import { Button, LinearProgress } from "@mui/material";
|
||||
// import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
import Modal, { IModalProps } from "@src/components/Modal";
|
||||
import { List, ListSubheader } from "@mui/material";
|
||||
import LogItem from "./LogItem";
|
||||
import CloudLogList from "./CloudLogList";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
export default function CloudLogsModal(props: IModalProps) {
|
||||
const { rowyRun } = useProjectContext();
|
||||
|
||||
const { data: logItems } = useSWR(
|
||||
const { data, mutate, isValidating } = useSWR(
|
||||
"logItems",
|
||||
() =>
|
||||
rowyRun
|
||||
@@ -19,64 +20,20 @@ export default function CloudLogsModal(props: IModalProps) {
|
||||
// path: "/logs",
|
||||
// path: '/logs?filter=resource.labels.function_name="R-githubStars"',
|
||||
path: `/logs?filter=logName="${encodeURIComponent(
|
||||
"projects/rowyio/logs/rowy-audit-logs"
|
||||
"projects/rowyio/logs/rowy-audit"
|
||||
)}"`,
|
||||
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>
|
||||
);
|
||||
{
|
||||
fallbackData: [],
|
||||
revalidateOnMount: true,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -84,11 +41,25 @@ export default function CloudLogsModal(props: IModalProps) {
|
||||
maxWidth="xl"
|
||||
fullWidth
|
||||
fullHeight
|
||||
title={`Cloud logs (${logItems?.length})`}
|
||||
ScrollableDialogContentProps={{ disableBottomDivider: true }}
|
||||
>
|
||||
<List component="ol" subheader={<li />}>
|
||||
{renderedLogItems}
|
||||
</List>
|
||||
<Button onClick={() => mutate()}>Refresh</Button>
|
||||
|
||||
{isValidating && (
|
||||
<LinearProgress
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "calc(var(--dialog-title-height) + 1px)",
|
||||
transform: "translateY(-100%)",
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderRadius: 0,
|
||||
marginTop: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Array.isArray(data) && <CloudLogList items={data} sx={{ mx: -1.5 }} />}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,6 +143,13 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"--dialog-title-height": "64px",
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
"--dialog-title-height": "56px",
|
||||
},
|
||||
},
|
||||
|
||||
paper: {
|
||||
borderRadius: (theme.shape.borderRadius as number) * 2,
|
||||
|
||||
|
||||
@@ -12141,6 +12141,11 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
pb-util@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pb-util/-/pb-util-1.0.1.tgz#1df63f3a7f7c6ee74bfb87f39df3420dbe0707eb"
|
||||
integrity sha512-cMm1ERTOTYb4LxAsLRN0oxhYTL3GivVItrBOiU4WSFcvaeynU/crNtHplNEGQIimSbl3/i7hxWflFvsx42tUVw==
|
||||
|
||||
pbkdf2@^3.0.3:
|
||||
version "3.0.17"
|
||||
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
|
||||
|
||||
Reference in New Issue
Block a user