diff --git a/package.json b/package.json index ad8cf3cc..6e2d86ea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Table/TableHeader/CloudLogs/CloudLogsModal.tsx b/src/components/Table/TableHeader/CloudLogs/CloudLogsModal.tsx new file mode 100644 index 00000000..820d8359 --- /dev/null +++ b/src/components/Table/TableHeader/CloudLogs/CloudLogsModal.tsx @@ -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[]>({ + 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( + + Function{" "} + {_get(logItem, "resource.labels.function_name")}{" "} + execution {_get(logItem, "labels.execution_id")} + + ); + } + + renderedLogItems.push( +
  • + +
  • + ); + } + } + + return ( + + }> + {renderedLogItems} + + + ); +} diff --git a/src/components/Table/TableHeader/CloudLogs/LogItem.tsx b/src/components/Table/TableHeader/CloudLogs/LogItem.tsx index 4ac1ed89..0b944ec9 100644 --- a/src/components/Table/TableHeader/CloudLogs/LogItem.tsx +++ b/src/components/Table/TableHeader/CloudLogs/LogItem.tsx @@ -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) => ( - -))({ +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) => )({ + 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; + 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 ( + + ); + }) + .filter(Boolean) + : []; -export default function LogItem(props: ILogItemProps) { return ( - + } - aria-controls="panel1bh-content" - id="panel1bh-header" - style={{ padding: 0 }} + aria-controls={`${data.insertId}-content`} + id={`${data.insertId}-header`} > - - General settings - - - I am an accordion + + + + + {renderedChips.length > 0 && ( + + {renderedChips} + + )} + + + {data.payload === "textPayload" && data.textPayload} + {_get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} + {data.payload === "jsonPayload" && JSON.stringify(data.jsonPayload)} + - - Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat. - Aliquam eget maximus est, id dignissim quam. - + ); diff --git a/src/components/Table/TableHeader/CloudLogs/LogSeverityIcon.tsx b/src/components/Table/TableHeader/CloudLogs/LogSeverityIcon.tsx new file mode 100644 index 00000000..58fc2dc3 --- /dev/null +++ b/src/components/Table/TableHeader/CloudLogs/LogSeverityIcon.tsx @@ -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 = ( + + + + ); + + switch (severity) { + case "DEBUG": + icon = ; + break; + + case "INFO": + icon = ; + break; + + case "NOTICE": + icon = ; + break; + + case "WARNING": + icon = ; + break; + + case "ERROR": + icon = ; + break; + + case "CRITICAL": + icon = ( + + + + ); + break; + + case "ALERT": + icon = ; + break; + + case "EMERGENCY": + icon = ; + break; + + default: + break; + } + + return ( + + {severity} +
    + {SEVERITY_LEVELS[severity]} + + } + describeChild + > + {icon} +
    + ); +} diff --git a/src/components/Table/TableHeader/CloudLogs/index.tsx b/src/components/Table/TableHeader/CloudLogs/index.tsx index e13d7128..015ffa9b 100644 --- a/src/components/Table/TableHeader/CloudLogs/index.tsx +++ b/src/components/Table/TableHeader/CloudLogs/index.tsx @@ -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>("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 && ( - setOpen(false)} - maxWidth="xl" - fullWidth - fullHeight - title="Cloud logs" - > - - - + setOpen(false)} title="Cloud logs" /> )} ); diff --git a/src/constants/dates.tsx b/src/constants/dates.tsx index c2c082ee..0b54090d 100644 --- a/src/constants/dates.tsx +++ b/src/constants/dates.tsx @@ -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; diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index 84f05a63..fc06ef74 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -80,9 +80,9 @@ interface IProjectContext { // A ref ot the import wizard. Prevents unnecessary re-renders importWizardRef: React.MutableRefObject; - rowyRun: ( + rowyRun: ( args: Omit - ) => Promise; + ) => Promise; } const ProjectContext = React.createContext>({}); diff --git a/yarn.lock b/yarn.lock index 0a60e954..2d972915 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"