mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
Merge pull request #1010 from rowyio/feature/functions-logging
ROWY-678: Rowy Logging
This commit is contained in:
@@ -142,14 +142,26 @@ export const selectedCellAtom = atom<SelectedCell | null>(null);
|
||||
export const contextMenuTargetAtom = atom<HTMLElement | null>(null);
|
||||
|
||||
export type CloudLogFilters = {
|
||||
type: "webhook" | "functions" | "audit" | "build";
|
||||
type: "extension" | "webhook" | "column" | "audit" | "build" | "functions";
|
||||
timeRange:
|
||||
| { type: "seconds" | "minutes" | "hours" | "days"; value: number }
|
||||
| { type: "range"; start: Date; end: Date };
|
||||
severity?: Array<keyof typeof SEVERITY_LEVELS>;
|
||||
webhook?: string[];
|
||||
extension?: string[];
|
||||
column?: string[];
|
||||
auditRowId?: string;
|
||||
buildLogExpanded?: number;
|
||||
functionType?: (
|
||||
| "connector"
|
||||
| "derivative-script"
|
||||
| "action"
|
||||
| "derivative-function"
|
||||
| "extension"
|
||||
| "defaultValue"
|
||||
| "hooks"
|
||||
)[];
|
||||
loggingSource?: ("backend-scripts" | "backend-function" | "hooks")[];
|
||||
};
|
||||
/** Store cloud log modal filters in URL */
|
||||
export const cloudLogFiltersAtom = atomWithHash<CloudLogFilters>(
|
||||
|
||||
@@ -38,6 +38,10 @@ export default function CodeEditorHelper({
|
||||
key: "rowy",
|
||||
description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`,
|
||||
},
|
||||
{
|
||||
key: "logging",
|
||||
description: `logging.log is encouraged to replace console.log`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
1
src/components/CodeEditor/extensions.d.ts
vendored
1
src/components/CodeEditor/extensions.d.ts
vendored
@@ -26,6 +26,7 @@ type ExtensionContext = {
|
||||
extensionBody: any;
|
||||
};
|
||||
RULES_UTILS: any;
|
||||
logging: RowyLogging;
|
||||
};
|
||||
|
||||
// extension body definition
|
||||
|
||||
5
src/components/CodeEditor/rowy.d.ts
vendored
5
src/components/CodeEditor/rowy.d.ts
vendored
@@ -17,6 +17,11 @@ type uploadOptions = {
|
||||
folderPath?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
type RowyLogging = {
|
||||
log: (payload: any) => void;
|
||||
warn: (payload: any) => void;
|
||||
error: (payload: any) => void;
|
||||
};
|
||||
interface Rowy {
|
||||
metadata: {
|
||||
/**
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
ColumnPlusBefore as ColumnPlusBeforeIcon,
|
||||
ColumnPlusAfter as ColumnPlusAfterIcon,
|
||||
ColumnRemove as ColumnRemoveIcon,
|
||||
CloudLogs as LogsIcon,
|
||||
} from "@src/assets/icons";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
// import ReorderIcon from "@mui/icons-material/Reorder";
|
||||
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import EvalIcon from "@mui/icons-material/PlayCircleOutline";
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
tableFiltersPopoverAtom,
|
||||
tableNextPageAtom,
|
||||
tableSchemaAtom,
|
||||
cloudLogFiltersAtom,
|
||||
tableModalAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
@@ -107,6 +109,8 @@ export default function ColumnMenu({
|
||||
);
|
||||
const [tableNextPage] = useAtom(tableNextPageAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const setModal = useSetAtom(tableModalAtom, tableScope);
|
||||
const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope);
|
||||
const snackLogContext = useSnackLogContext();
|
||||
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
@@ -314,26 +318,29 @@ export default function ColumnMenu({
|
||||
},
|
||||
disabled: !isConfigurable,
|
||||
},
|
||||
// {
|
||||
// label: "Re-order",
|
||||
// icon: <ReorderIcon />,
|
||||
// onClick: () => alert("REORDER"),
|
||||
// },
|
||||
|
||||
// {
|
||||
// label: "Hide for everyone",
|
||||
// activeLabel: "Show",
|
||||
// icon: <VisibilityOffIcon />,
|
||||
// activeIcon: <VisibilityIcon />,
|
||||
// onClick: () => {
|
||||
// actions.update(column.key, { hidden: !column.hidden });
|
||||
// handleClose();
|
||||
// },
|
||||
// active: column.hidden,
|
||||
// color: "error" as "error",
|
||||
// },
|
||||
];
|
||||
|
||||
if (
|
||||
column?.config?.defaultValue?.type === "dynamic" ||
|
||||
[FieldType.action, FieldType.derivative, FieldType.connector].includes(
|
||||
column.type
|
||||
)
|
||||
) {
|
||||
configActions.push({
|
||||
key: "logs",
|
||||
label: altPress ? "Logs" : "Logs…",
|
||||
icon: <LogsIcon />,
|
||||
onClick: () => {
|
||||
setModal("cloudLogs");
|
||||
setCloudLogFilters({
|
||||
type: "column",
|
||||
timeRange: { type: "days", value: 7 },
|
||||
column: [column.key],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Generalize
|
||||
const handleEvaluateAll = async () => {
|
||||
try {
|
||||
|
||||
@@ -52,11 +52,11 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) {
|
||||
} else if (column.config?.defaultValue?.dynamicValueFn) {
|
||||
dynamicValueFn = column.config?.defaultValue?.dynamicValueFn;
|
||||
} else if (column.config?.defaultValue?.script) {
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{
|
||||
${column.config?.defaultValue.script}
|
||||
}`;
|
||||
} else {
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{
|
||||
dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{
|
||||
// Write your default value code here
|
||||
// for example:
|
||||
// generate random hex color
|
||||
|
||||
@@ -4,5 +4,6 @@ type DefaultValueContext = {
|
||||
storage: firebasestorage.Storage;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
auth: firebaseauth.BaseAuth;
|
||||
logging: RowyLogging;
|
||||
};
|
||||
type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE";
|
||||
|
||||
@@ -187,22 +187,32 @@ export default function CloudLogItem({
|
||||
)}
|
||||
|
||||
<Typography variant="inherit" noWrap className="log-preview">
|
||||
{data.payload === "textPayload" && data.textPayload}
|
||||
{get(data, "httpRequest.requestUrl")?.split(".run.app").pop()}
|
||||
{data.payload === "jsonPayload" && (
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color="error"
|
||||
fontWeight="bold"
|
||||
component="span"
|
||||
>
|
||||
{data.jsonPayload.error}{" "}
|
||||
</Typography>
|
||||
{data.logName.endsWith("rowy-logging") && data.jsonPayload.payload ? (
|
||||
<>
|
||||
{typeof data.jsonPayload.payload === "string"
|
||||
? data.jsonPayload.payload
|
||||
: JSON.stringify(data.jsonPayload.payload)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data.payload === "textPayload" && data.textPayload}
|
||||
{get(data, "httpRequest.requestUrl")?.split(".run.app").pop()}
|
||||
{data.payload === "jsonPayload" && (
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color="error"
|
||||
fontWeight="bold"
|
||||
component="span"
|
||||
>
|
||||
{data.jsonPayload.error}{" "}
|
||||
</Typography>
|
||||
)}
|
||||
{data.payload === "jsonPayload" &&
|
||||
stringify(data.jsonPayload.body ?? data.jsonPayload, {
|
||||
space: 2,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{data.payload === "jsonPayload" &&
|
||||
stringify(data.jsonPayload.body ?? data.jsonPayload, {
|
||||
space: 2,
|
||||
})}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
|
||||
@@ -70,6 +70,13 @@ export default function CloudLogList({ items, ...props }: ICloudLogListProps) {
|
||||
"jsonPayload.rowyUser.displayName",
|
||||
// Webhook event
|
||||
"jsonPayload.params.endpoint",
|
||||
// Rowy Logging
|
||||
"jsonPayload.functionType",
|
||||
"jsonPayload.loggingSource",
|
||||
"jsonPayload.extensionName",
|
||||
"jsonPayload.extensionType",
|
||||
"jsonPayload.webhookName",
|
||||
"jsonPayload.fieldName",
|
||||
]}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -22,6 +22,12 @@ export const SEVERITY_LEVELS = {
|
||||
EMERGENCY: "One or more systems are unusable.",
|
||||
};
|
||||
|
||||
export const SEVERITY_LEVELS_ROWY = {
|
||||
DEFAULT: "The log entry has no assigned severity level.",
|
||||
WARNING: "Warning events might cause problems.",
|
||||
ERROR: "Error events are likely to cause problems.",
|
||||
};
|
||||
|
||||
export interface ICloudLogSeverityIconProps extends SvgIconProps {
|
||||
severity: keyof typeof SEVERITY_LEVELS;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useSWR from "swr";
|
||||
import { useAtom } from "jotai";
|
||||
import { startCase } from "lodash-es";
|
||||
import { startCase, upperCase } from "lodash-es";
|
||||
import { ITableModalProps } from "@src/components/TableModals";
|
||||
|
||||
import {
|
||||
@@ -12,9 +12,12 @@ import {
|
||||
TextField,
|
||||
InputAdornment,
|
||||
Button,
|
||||
Box,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { CloudLogs as LogsIcon } from "@src/assets/icons";
|
||||
import ClearIcon from "@mui/icons-material/Clear";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import TableToolbarButton from "@src/components/TableToolbar/TableToolbarButton";
|
||||
@@ -23,7 +26,10 @@ import TimeRangeSelect from "./TimeRangeSelect";
|
||||
import CloudLogList from "./CloudLogList";
|
||||
import BuildLogs from "./BuildLogs";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import CloudLogSeverityIcon, { SEVERITY_LEVELS } from "./CloudLogSeverityIcon";
|
||||
import CloudLogSeverityIcon, {
|
||||
SEVERITY_LEVELS,
|
||||
SEVERITY_LEVELS_ROWY,
|
||||
} from "./CloudLogSeverityIcon";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
@@ -38,6 +44,7 @@ import {
|
||||
cloudLogFiltersAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { cloudLogFetcher } from "./utils";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
|
||||
export default function CloudLogsModal({ onClose }: ITableModalProps) {
|
||||
const [projectId] = useAtom(projectIdAtom, projectScope);
|
||||
@@ -92,7 +99,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) {
|
||||
"&, & .MuiTab-root": {
|
||||
minHeight: { md: "var(--dialog-title-height)" },
|
||||
},
|
||||
ml: { md: 18 },
|
||||
ml: { md: 20 },
|
||||
mr: { md: 40 / 8 + 3 },
|
||||
|
||||
minHeight: 32,
|
||||
@@ -110,18 +117,35 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) {
|
||||
<ToggleButtonGroup
|
||||
value={cloudLogFilters.type}
|
||||
exclusive
|
||||
onChange={(_, v) =>
|
||||
onChange={(_, newType) => {
|
||||
setCloudLogFilters((c) => ({
|
||||
type: v,
|
||||
type: newType,
|
||||
timeRange: c.timeRange,
|
||||
}))
|
||||
}
|
||||
}));
|
||||
if (
|
||||
[
|
||||
"extension",
|
||||
"webhook",
|
||||
"column",
|
||||
"audit",
|
||||
"functions",
|
||||
].includes(newType)
|
||||
) {
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
aria-label="Filter by log type"
|
||||
>
|
||||
<ToggleButton value="webhook">Webhooks</ToggleButton>
|
||||
<ToggleButton value="functions">Functions</ToggleButton>
|
||||
<ToggleButton value="extension">Extension</ToggleButton>
|
||||
<ToggleButton value="webhook">Webhook</ToggleButton>
|
||||
<ToggleButton value="column">Column</ToggleButton>
|
||||
<ToggleButton value="audit">Audit</ToggleButton>
|
||||
<ToggleButton value="build">Build</ToggleButton>
|
||||
<ToggleButton value="functions">
|
||||
Functions (legacy)
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
) : (
|
||||
<ToggleButtonGroup
|
||||
@@ -139,209 +163,372 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) {
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
|
||||
{cloudLogFilters.type === "webhook" && (
|
||||
<MultiSelect
|
||||
multiple
|
||||
label="Webhook:"
|
||||
labelPlural="webhooks"
|
||||
options={
|
||||
Array.isArray(tableSchema.webhooks)
|
||||
? tableSchema.webhooks.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.endpoint,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
value={cloudLogFilters.webhook ?? []}
|
||||
onChange={(v) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, webhook: v }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
id: "webhook",
|
||||
className: "labelHorizontal",
|
||||
sx: { "& .MuiInputBase-root": { width: 180 } },
|
||||
fullWidth: false,
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
{option.label} <code>{option.value}</code>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{cloudLogFilters.type === "audit" && (
|
||||
<TextField
|
||||
id="auditRowId"
|
||||
label="Row ID:"
|
||||
value={cloudLogFilters.auditRowId}
|
||||
onChange={(e) =>
|
||||
setCloudLogFilters((prev) => ({
|
||||
...prev,
|
||||
auditRowId: e.target.value,
|
||||
}))
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{tableSettings.collection}/
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
className="labelHorizontal"
|
||||
sx={{
|
||||
"& .MuiInputBase-root, & .MuiInputBase-input": {
|
||||
typography: "body2",
|
||||
fontFamily: "mono",
|
||||
},
|
||||
"& .MuiInputAdornment-positionStart": {
|
||||
m: "0 !important",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
"& .MuiInputBase-input": { pl: 0 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
{cloudLogFilters.type !== "build" && (
|
||||
<>
|
||||
{!isValidating && Array.isArray(data) && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.disabled"
|
||||
display="block"
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{data.length} entries
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<MultiSelect
|
||||
aria-label="Severity"
|
||||
labelPlural="severity levels"
|
||||
options={Object.keys(SEVERITY_LEVELS)}
|
||||
value={cloudLogFilters.severity ?? []}
|
||||
onChange={(severity) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, severity }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
style: { width: 130 },
|
||||
placeholder: "Severity",
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (
|
||||
!Array.isArray(cloudLogFilters.severity) ||
|
||||
cloudLogFilters.severity.length === 0
|
||||
)
|
||||
return `Severity`;
|
||||
|
||||
if (cloudLogFilters.severity.length === 1)
|
||||
return (
|
||||
<>
|
||||
Severity{" "}
|
||||
<CloudLogSeverityIcon
|
||||
severity={cloudLogFilters.severity[0]}
|
||||
style={{ marginTop: -2, marginBottom: -7 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return `Severity (${cloudLogFilters.severity.length})`;
|
||||
},
|
||||
},
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.disabled"
|
||||
display="block"
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
{isValidating ? "" : `${data?.length ?? 0} entries`}
|
||||
</Typography>
|
||||
<TableToolbarButton
|
||||
onClick={() => {
|
||||
setCloudLogFilters((prev) => ({
|
||||
...prev,
|
||||
functionType: undefined,
|
||||
loggingSource: undefined,
|
||||
webhook: undefined,
|
||||
extension: undefined,
|
||||
severity: undefined,
|
||||
}));
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
<CloudLogSeverityIcon
|
||||
severity={option.value}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
{startCase(option.value.toLowerCase())}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<TimeRangeSelect
|
||||
aria-label="Time range"
|
||||
value={cloudLogFilters.timeRange}
|
||||
onChange={(value) =>
|
||||
setCloudLogFilters((c) => ({ ...c, timeRange: value }))
|
||||
}
|
||||
title="Clear Filters"
|
||||
icon={<ClearIcon />}
|
||||
disabled={isValidating}
|
||||
/>
|
||||
<TableToolbarButton
|
||||
onClick={() => mutate()}
|
||||
title="Refresh"
|
||||
icon={<RefreshIcon />}
|
||||
icon={
|
||||
isValidating ? (
|
||||
<CircularProgress size={15} thickness={4} />
|
||||
) : (
|
||||
<RefreshIcon />
|
||||
)
|
||||
}
|
||||
disabled={isValidating}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{isValidating && (
|
||||
<LinearProgress
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
marginTop: -4,
|
||||
marginBottom: -1,
|
||||
minHeight: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <code>{logQueryUrl}</code> */}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{cloudLogFilters.type === "build" ? (
|
||||
<BuildLogs />
|
||||
) : Array.isArray(data) && data.length > 0 ? (
|
||||
<>
|
||||
<CloudLogList items={data} sx={{ mx: -1.5, mt: 1.5 }} />
|
||||
{cloudLogFilters.timeRange.type !== "range" && (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
display: "flex",
|
||||
}}
|
||||
onClick={() =>
|
||||
setCloudLogFilters((c) => ({
|
||||
...c,
|
||||
timeRange: {
|
||||
...c.timeRange,
|
||||
value: (c.timeRange as any).value * 2,
|
||||
},
|
||||
}))
|
||||
}
|
||||
>
|
||||
Load more (last {cloudLogFilters.timeRange.value * 2}{" "}
|
||||
{cloudLogFilters.timeRange.type})
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : isValidating ? (
|
||||
<EmptyState
|
||||
Icon={LogsIcon}
|
||||
message="Fetching logs…"
|
||||
description={"\xa0"}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
Icon={LogsIcon}
|
||||
message="No logs"
|
||||
description={
|
||||
cloudLogFilters.type === "webhook" &&
|
||||
(!Array.isArray(tableSchema.webhooks) ||
|
||||
tableSchema.webhooks?.length === 0)
|
||||
? "There are no webhooks in this table"
|
||||
: cloudLogFilters.type === "audit" &&
|
||||
tableSettings.audit === false
|
||||
? "Auditing is disabled in this table"
|
||||
: "\xa0"
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflowY: "visible",
|
||||
}}
|
||||
>
|
||||
{["extension", "webhook", "column", "audit", "functions"].includes(
|
||||
cloudLogFilters.type
|
||||
) ? (
|
||||
<Stack
|
||||
width={"100%"}
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
margin: "8px 0",
|
||||
flex: "0 0 32px",
|
||||
}}
|
||||
>
|
||||
{cloudLogFilters.type === "functions" ? (
|
||||
<Box width={"100%"}></Box>
|
||||
) : null}
|
||||
{cloudLogFilters.type === "extension" ? (
|
||||
<>
|
||||
<MultiSelect
|
||||
multiple
|
||||
aria-label={"Extension"}
|
||||
labelPlural="extensions"
|
||||
options={
|
||||
Array.isArray(tableSchema.extensionObjects)
|
||||
? tableSchema.extensionObjects.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.name,
|
||||
type: x.type,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
value={cloudLogFilters.extension ?? []}
|
||||
onChange={(v) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, extension: v }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
id: "extension",
|
||||
className: "labelHorizontal",
|
||||
sx: {
|
||||
width: "100%",
|
||||
"& .MuiInputBase-root": { width: "100%" },
|
||||
},
|
||||
fullWidth: false,
|
||||
placeholder: "Extension",
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (cloudLogFilters?.extension?.length === 1) {
|
||||
return `Extension (${cloudLogFilters.extension[0]})`;
|
||||
} else if (cloudLogFilters?.extension?.length) {
|
||||
return `Extension (${cloudLogFilters.extension.length})`;
|
||||
} else {
|
||||
return `Extension`;
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
{option.label} <code>{option.type}</code>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{cloudLogFilters.type === "webhook" ? (
|
||||
<MultiSelect
|
||||
multiple
|
||||
aria-label="Webhook:"
|
||||
labelPlural="webhooks"
|
||||
options={
|
||||
Array.isArray(tableSchema.webhooks)
|
||||
? tableSchema.webhooks.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.endpoint,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
value={cloudLogFilters.webhook ?? []}
|
||||
onChange={(v) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, webhook: v }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
id: "webhook",
|
||||
className: "labelHorizontal",
|
||||
sx: {
|
||||
width: "100%",
|
||||
"& .MuiInputBase-root": { width: "100%" },
|
||||
},
|
||||
fullWidth: false,
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (cloudLogFilters?.webhook?.length) {
|
||||
return `Webhook (${cloudLogFilters.webhook.length})`;
|
||||
} else {
|
||||
return `Webhook`;
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
{option.label} <code>{option.value}</code>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
{cloudLogFilters.type === "column" ? (
|
||||
<>
|
||||
<MultiSelect
|
||||
multiple
|
||||
aria-label={"Column"}
|
||||
options={Object.entries(tableSchema.columns ?? {})
|
||||
.filter(
|
||||
([key, config]) =>
|
||||
config?.config?.defaultValue?.type === "dynamic" ||
|
||||
[
|
||||
FieldType.action,
|
||||
FieldType.derivative,
|
||||
FieldType.connector,
|
||||
].includes(config.type)
|
||||
)
|
||||
.map(([key, config]) => ({
|
||||
label: config.name,
|
||||
value: key,
|
||||
type: config.type,
|
||||
}))}
|
||||
value={cloudLogFilters.column ?? []}
|
||||
onChange={(v) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, column: v }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
id: "column",
|
||||
className: "labelHorizontal",
|
||||
sx: {
|
||||
width: "100%",
|
||||
"& .MuiInputBase-root": { width: "100%" },
|
||||
},
|
||||
fullWidth: false,
|
||||
placeholder: "Column",
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (cloudLogFilters?.column?.length === 1) {
|
||||
return `Column (${cloudLogFilters.column[0]})`;
|
||||
} else if (cloudLogFilters?.column?.length) {
|
||||
return `Column (${cloudLogFilters.column.length})`;
|
||||
} else {
|
||||
return `Column`;
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
{option.label} <code>{option.value}</code>
|
||||
<code>{option.type}</code>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{cloudLogFilters.type === "audit" ? (
|
||||
<>
|
||||
<TextField
|
||||
id="auditRowId"
|
||||
label="Row ID:"
|
||||
value={cloudLogFilters.auditRowId}
|
||||
onChange={(e) =>
|
||||
setCloudLogFilters((prev) => ({
|
||||
...prev,
|
||||
auditRowId: e.target.value,
|
||||
}))
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{tableSettings.collection}/
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
className="labelHorizontal"
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& .MuiInputBase-root, & .MuiInputBase-input": {
|
||||
width: "100%",
|
||||
typography: "body2",
|
||||
fontFamily: "mono",
|
||||
},
|
||||
"& .MuiInputAdornment-positionStart": {
|
||||
m: "0 !important",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
"& .MuiInputBase-input": { pl: 0 },
|
||||
"& .MuiFormLabel-root": {
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<MultiSelect
|
||||
aria-label="Severity"
|
||||
labelPlural="severity levels"
|
||||
options={Object.keys(SEVERITY_LEVELS_ROWY)}
|
||||
value={cloudLogFilters.severity ?? []}
|
||||
onChange={(severity) =>
|
||||
setCloudLogFilters((prev) => ({ ...prev, severity }))
|
||||
}
|
||||
TextFieldProps={{
|
||||
style: { width: 200 },
|
||||
placeholder: "Severity",
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (
|
||||
!Array.isArray(cloudLogFilters.severity) ||
|
||||
cloudLogFilters.severity.length === 0
|
||||
)
|
||||
return `Severity`;
|
||||
|
||||
if (cloudLogFilters.severity.length === 1)
|
||||
return (
|
||||
<>
|
||||
Severity{" "}
|
||||
<CloudLogSeverityIcon
|
||||
severity={cloudLogFilters.severity[0]}
|
||||
style={{ marginTop: -2, marginBottom: -7 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return `Severity (${cloudLogFilters.severity.length})`;
|
||||
},
|
||||
},
|
||||
}}
|
||||
itemRenderer={(option) => (
|
||||
<>
|
||||
<CloudLogSeverityIcon
|
||||
severity={option.value}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
{startCase(option.value.toLowerCase())}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<TimeRangeSelect
|
||||
aria-label="Time range"
|
||||
value={cloudLogFilters.timeRange}
|
||||
onChange={(value) =>
|
||||
setCloudLogFilters((c) => ({ ...c, timeRange: value }))
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
) : null}
|
||||
<Box
|
||||
sx={{
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
{Array.isArray(data) && data.length > 0 ? (
|
||||
<Box>
|
||||
<CloudLogList items={data} sx={{ mx: -1.5, mt: 1.5 }} />
|
||||
{cloudLogFilters.timeRange.type !== "range" && (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
display: "flex",
|
||||
}}
|
||||
onClick={() => {
|
||||
setCloudLogFilters((c) => ({
|
||||
...c,
|
||||
timeRange: {
|
||||
...c.timeRange,
|
||||
value: (c.timeRange as any).value * 2,
|
||||
},
|
||||
}));
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
Load more (last {cloudLogFilters.timeRange.value * 2}{" "}
|
||||
{cloudLogFilters.timeRange.type})
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
) : isValidating ? (
|
||||
<EmptyState
|
||||
Icon={LogsIcon}
|
||||
message="Fetching logs…"
|
||||
description={"\xa0"}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
Icon={LogsIcon}
|
||||
message="No logs"
|
||||
description={
|
||||
cloudLogFilters.type !== "audit"
|
||||
? "There are no logs matching the filters"
|
||||
: cloudLogFilters.type === "audit" &&
|
||||
tableSettings.audit === false
|
||||
? "Auditing is disabled in this table"
|
||||
: "\xa0"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,9 @@ export default function TimeRangeSelect({
|
||||
...props
|
||||
}: ITimeRangeSelectProps) {
|
||||
return (
|
||||
<fieldset style={{ appearance: "none", padding: 0, border: 0 }}>
|
||||
<fieldset
|
||||
style={{ appearance: "none", padding: 0, border: 0, display: "flex" }}
|
||||
>
|
||||
{value && value.type !== "range" && (
|
||||
<TextField
|
||||
aria-label={`Custom ${value.type} value`}
|
||||
|
||||
@@ -12,21 +12,69 @@ export const cloudLogFetcher = (
|
||||
// https://cloud.google.com/logging/docs/view/logging-query-language
|
||||
let logQuery: string[] = [];
|
||||
|
||||
if (["extension", "webhook", "column"].includes(cloudLogFilters.type)) {
|
||||
// mandatory filter to remove unwanted gcp diagnostic logs
|
||||
logQuery.push(
|
||||
["backend-scripts", "backend-function", "hooks"]
|
||||
.map((loggingSource) => {
|
||||
return `jsonPayload.loggingSource = "${loggingSource}"`;
|
||||
})
|
||||
.join(encodeURIComponent(" OR "))
|
||||
);
|
||||
}
|
||||
|
||||
switch (cloudLogFilters.type) {
|
||||
case "extension":
|
||||
logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`);
|
||||
if (cloudLogFilters?.extension?.length) {
|
||||
logQuery.push(
|
||||
cloudLogFilters.extension
|
||||
.map((extensionName) => {
|
||||
return `jsonPayload.extensionName = "${extensionName}"`;
|
||||
})
|
||||
.join(encodeURIComponent(" OR "))
|
||||
);
|
||||
} else {
|
||||
logQuery.push(`jsonPayload.functionType = "extension"`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "webhook":
|
||||
logQuery.push(
|
||||
`logName = "projects/${projectId}/logs/rowy-webhook-events"`
|
||||
);
|
||||
logQuery.push(`jsonPayload.url : "${tablePath}"`);
|
||||
if (
|
||||
Array.isArray(cloudLogFilters.webhook) &&
|
||||
cloudLogFilters.webhook.length > 0
|
||||
)
|
||||
if (cloudLogFilters?.webhook?.length) {
|
||||
logQuery.push(
|
||||
cloudLogFilters.webhook
|
||||
.map((id) => `jsonPayload.url : "${id}"`)
|
||||
.join(encodeURIComponent(" OR "))
|
||||
);
|
||||
} else {
|
||||
logQuery.push(`jsonPayload.functionType = "hooks"`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "column":
|
||||
if (cloudLogFilters?.column?.length) {
|
||||
logQuery.push(
|
||||
cloudLogFilters.column
|
||||
.map((column) => {
|
||||
return `jsonPayload.fieldName = "${column}"`;
|
||||
})
|
||||
.join(encodeURIComponent(" OR "))
|
||||
);
|
||||
} else {
|
||||
logQuery.push(
|
||||
[
|
||||
"connector",
|
||||
"derivative-script",
|
||||
"action",
|
||||
"derivative-function",
|
||||
"defaultValue",
|
||||
]
|
||||
.map((functionType) => {
|
||||
return `jsonPayload.functionType = "${functionType}"`;
|
||||
})
|
||||
.join(encodeURIComponent(" OR "))
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "audit":
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
Extension as ExtensionIcon,
|
||||
Copy as DuplicateIcon,
|
||||
CloudLogs as LogsIcon,
|
||||
} from "@src/assets/icons";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
@@ -21,6 +22,12 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import { extensionNames, IExtension } from "./utils";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
cloudLogFiltersAtom,
|
||||
tableModalAtom,
|
||||
tableScope,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
export interface IExtensionListProps {
|
||||
extensions: IExtension[];
|
||||
@@ -37,6 +44,9 @@ export default function ExtensionList({
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}: IExtensionListProps) {
|
||||
const setModal = useSetAtom(tableModalAtom, tableScope);
|
||||
const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope);
|
||||
|
||||
if (extensions.length === 0)
|
||||
return (
|
||||
<EmptyState
|
||||
@@ -91,6 +101,21 @@ export default function ExtensionList({
|
||||
<DuplicateIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Logs">
|
||||
<IconButton
|
||||
aria-label="Logs"
|
||||
onClick={() => {
|
||||
setModal("cloudLogs");
|
||||
setCloudLogFilters({
|
||||
type: "extension",
|
||||
timeRange: { type: "days", value: 7 },
|
||||
extension: [extensionObject.name],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LogsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
aria-label="Edit"
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface IRuntimeOptions {
|
||||
export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"];
|
||||
|
||||
const extensionBodyTemplate = {
|
||||
task: `const extensionBody: TaskBody = async({row, db, change, ref}) => {
|
||||
task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => {
|
||||
// task extensions are very flexible you can do anything from updating other documents in your database, to making an api request to 3rd party service.
|
||||
|
||||
// example:
|
||||
@@ -87,7 +87,7 @@ const extensionBodyTemplate = {
|
||||
})
|
||||
*/
|
||||
}`,
|
||||
docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref}) => {
|
||||
docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -96,7 +96,7 @@ const extensionBodyTemplate = {
|
||||
targetPath: "", // fill in the path here
|
||||
})
|
||||
}`,
|
||||
historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref}) => {
|
||||
historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -104,7 +104,7 @@ const extensionBodyTemplate = {
|
||||
collectionId: "historySnapshots", // optionally change the sub-collection id of where the history snapshots are stored
|
||||
})
|
||||
}`,
|
||||
algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref}) => {
|
||||
algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -114,7 +114,7 @@ const extensionBodyTemplate = {
|
||||
objectID: ref.id, // algolia object ID, ref.id is one possible choice
|
||||
})
|
||||
}`,
|
||||
meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref}) => {
|
||||
meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return({
|
||||
@@ -124,7 +124,7 @@ const extensionBodyTemplate = {
|
||||
objectID: ref.id, // algolia object ID, ref.id is one possible choice
|
||||
})
|
||||
}`,
|
||||
bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref}) => {
|
||||
bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -134,7 +134,7 @@ const extensionBodyTemplate = {
|
||||
objectID: ref.id, // algolia object ID, ref.id is one possible choice
|
||||
})
|
||||
}`,
|
||||
slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref}) => {
|
||||
slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -144,7 +144,7 @@ const extensionBodyTemplate = {
|
||||
attachments: [], // the attachments parameter to pass in to slack api
|
||||
})
|
||||
}`,
|
||||
sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref}) => {
|
||||
sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -164,7 +164,7 @@ const extensionBodyTemplate = {
|
||||
},
|
||||
})
|
||||
}`,
|
||||
apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref}) => {
|
||||
apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
|
||||
return ({
|
||||
@@ -174,7 +174,7 @@ const extensionBodyTemplate = {
|
||||
callback: ()=>{},
|
||||
})
|
||||
}`,
|
||||
twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref}) => {
|
||||
twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => {
|
||||
/**
|
||||
*
|
||||
* Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup
|
||||
@@ -190,7 +190,7 @@ const extensionBodyTemplate = {
|
||||
body: "Hi there!" // message text
|
||||
})
|
||||
}`,
|
||||
pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref}) => {
|
||||
pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => {
|
||||
// you can FCM token from the row or from the user document in the database
|
||||
// const FCMtoken = row.FCMtoken
|
||||
// or push through topic
|
||||
@@ -238,13 +238,14 @@ export function emptyExtensionObject(
|
||||
extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"],
|
||||
requiredFields: [],
|
||||
trackedFields: [],
|
||||
conditions: `const condition: Condition = async({row, change}) => {
|
||||
conditions: `const condition: Condition = async({row, change, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
lastEditor: user,
|
||||
};
|
||||
}
|
||||
|
||||
export function sparkToExtensionObjects(
|
||||
sparkConfig: string,
|
||||
user: IExtensionEditor
|
||||
|
||||
@@ -21,17 +21,33 @@ const requestType = [
|
||||
|
||||
export const parserExtraLibs = [
|
||||
requestType,
|
||||
`type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{
|
||||
send:(v:any)=>void
|
||||
sendStatus:(status:number)=>void
|
||||
}}) => Promise<any>;`,
|
||||
`type Parser = (
|
||||
args: {
|
||||
req: WebHookRequest;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
ref: FirebaseFirestore.CollectionReference;
|
||||
res: {
|
||||
send: (v:any)=>void;
|
||||
sendStatus: (status:number)=>void
|
||||
};
|
||||
logging: RowyLogging;
|
||||
}
|
||||
) => Promise<any>;`,
|
||||
];
|
||||
export const conditionExtraLibs = [
|
||||
requestType,
|
||||
`type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{
|
||||
send:(v:any)=>void
|
||||
sendStatus:(status:number)=>void
|
||||
}}) => Promise<any>;`,
|
||||
`type Condition = (
|
||||
args: {
|
||||
req: WebHookRequest;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
ref: FirebaseFirestore.CollectionReference;
|
||||
res: {
|
||||
send: (v:any)=>void;
|
||||
sendStatus: (status:number)=>void;
|
||||
};
|
||||
logging: RowyLogging;
|
||||
}
|
||||
) => Promise<any>;`,
|
||||
];
|
||||
|
||||
const additionalVariables = [
|
||||
@@ -48,30 +64,29 @@ export const webhookBasic = {
|
||||
extraLibs: parserExtraLibs,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const basicParser: Parser = async({req, db,ref}) => {
|
||||
// request is the request object from the webhook
|
||||
// db is the database object
|
||||
// ref is the reference to collection of the table
|
||||
// the returned object will be added as a new row to the table
|
||||
// eg: adding the webhook body as row
|
||||
const {body} = req;
|
||||
${
|
||||
table.audit !== false
|
||||
? `
|
||||
// auditField
|
||||
const ${
|
||||
table.auditFieldCreatedBy ?? "_createdBy"
|
||||
} = await rowy.metadata.serviceAccountUser()
|
||||
return {
|
||||
...body,
|
||||
${table.auditFieldCreatedBy ?? "_createdBy"}
|
||||
}
|
||||
`
|
||||
: `
|
||||
return body;
|
||||
`
|
||||
}
|
||||
|
||||
) => `const basicParser: Parser = async({req, db, ref, logging}) => {
|
||||
// request is the request object from the webhook
|
||||
// db is the database object
|
||||
// ref is the reference to collection of the table
|
||||
// the returned object will be added as a new row to the table
|
||||
// eg: adding the webhook body as row
|
||||
const {body} = req;
|
||||
${
|
||||
table.audit !== false
|
||||
? `
|
||||
// auditField
|
||||
const ${
|
||||
table.auditFieldCreatedBy ?? "_createdBy"
|
||||
} = await rowy.metadata.serviceAccountUser()
|
||||
return {
|
||||
...body,
|
||||
${table.auditFieldCreatedBy ?? "_createdBy"}
|
||||
}
|
||||
`
|
||||
: `
|
||||
return body;
|
||||
`
|
||||
}
|
||||
}`,
|
||||
},
|
||||
condition: {
|
||||
@@ -79,7 +94,7 @@ export const webhookBasic = {
|
||||
extraLibs: conditionExtraLibs,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
) => `const condition: Condition = async({ref, req, db, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const webhookSendgrid = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const sendgridParser: Parser = async ({ req, db, ref }) => {
|
||||
) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => {
|
||||
const { body } = req
|
||||
const eventHandler = async (sgEvent) => {
|
||||
// Event handlers can be modiefed to preform different actions based on the sendgrid event
|
||||
@@ -35,7 +35,7 @@ export const webhookSendgrid = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
) => `const condition: Condition = async({ref, req, db, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const webhookStripe = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const sendgridParser: Parser = async ({ req, db, ref }) => {
|
||||
) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => {
|
||||
const event = req.body
|
||||
switch (event.type) {
|
||||
case "payment_intent.succeeded":
|
||||
@@ -34,7 +34,7 @@ export const webhookStripe = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
) => `const condition: Condition = async({ref, req, db, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
|
||||
@@ -13,7 +13,7 @@ export const webhookTypeform = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const typeformParser: Parser = async({req, db,ref}) =>{
|
||||
) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{
|
||||
// this reduces the form submission into a single object of key value pairs
|
||||
// eg: {name: "John", age: 20}
|
||||
// ⚠️ ensure that you have assigned ref values of the fields
|
||||
@@ -73,7 +73,7 @@ export const webhookTypeform = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
) => `const condition: Condition = async({ref, req, db, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
|
||||
@@ -14,7 +14,7 @@ export const webhook = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const formParser: Parser = async({req, db,ref}) => {
|
||||
) => `const formParser: Parser = async({req, db, ref, logging}) => {
|
||||
// request is the request object from the webhook
|
||||
// db is the database object
|
||||
// ref is the reference to collection of the table
|
||||
@@ -45,7 +45,7 @@ export const webhook = {
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
) => `const condition: Condition = async({ref, req, db, logging}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
|
||||
@@ -26,17 +26,33 @@ const requestType = [
|
||||
|
||||
export const parserExtraLibs = [
|
||||
requestType,
|
||||
`type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{
|
||||
send:(v:any)=>void
|
||||
sendStatus:(status:number)=>void
|
||||
}}) => Promise<any>;`,
|
||||
`type Parser = (
|
||||
args: {
|
||||
req: WebHookRequest;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
ref: FirebaseFirestore.CollectionReference;
|
||||
res: {
|
||||
send: (v:any)=>void;
|
||||
sendStatus: (status:number)=>void
|
||||
};
|
||||
logging: RowyLogging;
|
||||
}
|
||||
) => Promise<any>;`,
|
||||
];
|
||||
export const conditionExtraLibs = [
|
||||
requestType,
|
||||
`type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{
|
||||
send:(v:any)=>void
|
||||
sendStatus:(status:number)=>void
|
||||
}}) => Promise<any>;`,
|
||||
`type Condition = (
|
||||
args: {
|
||||
req:WebHookRequest,
|
||||
db: FirebaseFirestore.Firestore,
|
||||
ref: FirebaseFirestore.CollectionReference,
|
||||
res: {
|
||||
send: (v:any)=>void
|
||||
sendStatus: (status:number)=>void
|
||||
};
|
||||
logging: RowyLogging;
|
||||
}
|
||||
) => Promise<any>;`,
|
||||
];
|
||||
|
||||
const additionalVariables = [
|
||||
|
||||
@@ -3,10 +3,12 @@ type Condition = (args: {
|
||||
db: FirebaseFirestore.Firestore;
|
||||
ref: FirebaseFirestore.CollectionReference;
|
||||
res: Response;
|
||||
logging: RowyLogging;
|
||||
}) => Promise<any>;
|
||||
|
||||
type Parser = (args: {
|
||||
req: WebHookRequest;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
ref: FirebaseFirestore.CollectionReference;
|
||||
logging: RowyLogging;
|
||||
}) => Promise<any>;
|
||||
|
||||
@@ -130,7 +130,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
: config?.runFn
|
||||
? config.runFn
|
||||
: config?.script
|
||||
? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => {
|
||||
? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => {
|
||||
${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")}
|
||||
}`
|
||||
: RUN_ACTION_TEMPLATE;
|
||||
@@ -140,7 +140,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
: config.undoFn
|
||||
? config.undoFn
|
||||
: get(config, "undo.script")
|
||||
? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => {
|
||||
? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => {
|
||||
${get(config, "undo.script")}
|
||||
}`
|
||||
: UNDO_ACTION_TEMPLATE;
|
||||
|
||||
1
src/components/fields/Action/action.d.ts
vendored
1
src/components/fields/Action/action.d.ts
vendored
@@ -15,6 +15,7 @@ type ActionContext = {
|
||||
auth: firebaseauth.BaseAuth;
|
||||
actionParams: actionParams;
|
||||
user: ActionUser;
|
||||
logging: RowyLogging;
|
||||
};
|
||||
|
||||
type ActionResult = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => {
|
||||
export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => {
|
||||
// Write your action code here
|
||||
// for example:
|
||||
// const authToken = await rowy.secrets.get("service")
|
||||
@@ -26,7 +26,7 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto
|
||||
// checkout the documentation for more info: https://docs.rowy.io/field-types/action#script
|
||||
}`;
|
||||
|
||||
export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => {
|
||||
export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => {
|
||||
// Write your undo code here
|
||||
// for example:
|
||||
// const authToken = await rowy.secrets.get("service")
|
||||
|
||||
@@ -15,6 +15,7 @@ type ConnectorContext = {
|
||||
auth: firebaseauth.BaseAuth;
|
||||
query: string;
|
||||
user: ConnectorUser;
|
||||
logging: RowyLogging;
|
||||
};
|
||||
type ConnectorResult = any[];
|
||||
type Connector = (
|
||||
|
||||
@@ -11,7 +11,7 @@ export const replacer = (data: any) => (m: string, key: string) => {
|
||||
return get(data, objKey, defaultValue);
|
||||
};
|
||||
|
||||
export const baseFunction = `const connectorFn: Connector = async ({query, row, user}) => {
|
||||
export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => {
|
||||
// TODO: Implement your service function here
|
||||
return [];
|
||||
};`;
|
||||
|
||||
@@ -65,10 +65,10 @@ export default function Settings({
|
||||
: config.derivativeFn
|
||||
? config.derivativeFn
|
||||
: config?.script
|
||||
? `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{
|
||||
? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{
|
||||
${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")}
|
||||
}`
|
||||
: `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{
|
||||
: `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{
|
||||
// Write your derivative code here
|
||||
// for example:
|
||||
// const sum = row.a + row.b;
|
||||
|
||||
@@ -5,6 +5,7 @@ type DerivativeContext = {
|
||||
db: FirebaseFirestore.Firestore;
|
||||
auth: firebaseauth.BaseAuth;
|
||||
change: any;
|
||||
logging: RowyLogging;
|
||||
};
|
||||
|
||||
type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE";
|
||||
|
||||
Reference in New Issue
Block a user