mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
Merge branch 'develop' into feat/cloud-logs
* develop: setup fixes auditing rr min version rowyRun version requirement fix monaco row definition auditing update cell audit logs remove components/Table/Settings remove old Table/Settings/Webhooks file standardize imports to use "@src/ fns log basic logs typeform template removed endpoint step basic webhooks
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-usestateref": "^1.0.5",
|
||||
"semver": "^7.3.5",
|
||||
"serve": "^11.3.2",
|
||||
"swr": "^1.0.1",
|
||||
"tinymce": "^5.9.2",
|
||||
|
||||
10
src/assets/icons/Webhook.tsx
Normal file
10
src/assets/icons/Webhook.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
import { mdiWebhook } from "@mdi/js";
|
||||
|
||||
export default function Webhook(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiWebhook} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -115,7 +115,10 @@ export default function useMonacoCustomizations({
|
||||
Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => {
|
||||
const column = tableState?.columns[columnKey];
|
||||
return `static ${columnKey}: ${getFieldProp("type", column.type)}`;
|
||||
return `static "${columnKey}": ${getFieldProp(
|
||||
"dataType",
|
||||
column.type
|
||||
)}`;
|
||||
})
|
||||
.join(";\n") + ";";
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Step2ServiceAccount({
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const [hasAllRoles, setHasAllRoles] = useState(completion.serviceAccount);
|
||||
const [roles, setRoles] = useState<Record<string, any>>({});
|
||||
// const [roles, setRoles] = useState<Record<string, any>>({});
|
||||
const [verificationStatus, setVerificationStatus] = useState<
|
||||
"IDLE" | "LOADING" | "FAIL"
|
||||
>("IDLE");
|
||||
@@ -40,7 +40,7 @@ export default function Step2ServiceAccount({
|
||||
setVerificationStatus("LOADING");
|
||||
try {
|
||||
const result = await checkServiceAccount(rowyRunUrl);
|
||||
setRoles(result);
|
||||
// setRoles(result);
|
||||
if (result.hasAllRoles) {
|
||||
setVerificationStatus("IDLE");
|
||||
setHasAllRoles(true);
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function Step4Rules({
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setHasRules(true);
|
||||
}
|
||||
setRulesStatus("IDLE");
|
||||
setRulesStatus("");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setRulesStatus(e.message);
|
||||
|
||||
@@ -61,11 +61,11 @@ export default function Step4Body({
|
||||
extensionBody: isValid,
|
||||
});
|
||||
}}
|
||||
diagnosticsOptions={{
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: true,
|
||||
}}
|
||||
// diagnosticsOptions={{
|
||||
// noSemanticValidation: false,
|
||||
// noSyntaxValidation: false,
|
||||
// noSuggestionDiagnostics: true,
|
||||
// }}
|
||||
onMount={() => setBodyEditorActive(true)}
|
||||
onUnmount={() => setBodyEditorActive(false)}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface IExtension {
|
||||
active: boolean;
|
||||
lastEditor: IExtensionEditor;
|
||||
|
||||
// ft build fields
|
||||
// build fields
|
||||
triggers: ExtensionTrigger[];
|
||||
type: ExtensionType;
|
||||
requiredFields: string[];
|
||||
|
||||
23
src/components/Table/TableHeader/Webhooks/Step1Secret.tsx
Normal file
23
src/components/Table/TableHeader/Webhooks/Step1Secret.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IWebhookModalStepProps } from "./WebhookModal";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { FormControl, FormLabel, TextField, Typography } from "@mui/material";
|
||||
|
||||
export default function Step1Endpoint({
|
||||
webhookObject,
|
||||
setWebhookObject,
|
||||
}: IWebhookModalStepProps) {
|
||||
return (
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" className="visually-hidden">
|
||||
Secret
|
||||
</FormLabel>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={webhookObject.secret}
|
||||
onChange={(e) =>
|
||||
setWebhookObject({ ...webhookObject, secret: e.target.value })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { IWebhookModalStepProps } from "./WebhookModal";
|
||||
import useStateRef from "react-usestateref";
|
||||
|
||||
import CodeEditor from "@src/components/CodeEditor";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
const additionalVariables = [
|
||||
{
|
||||
key: "req",
|
||||
description: "webhook request",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Step3Conditions({
|
||||
webhookObject,
|
||||
setWebhookObject,
|
||||
setValidation,
|
||||
validationRef,
|
||||
}: IWebhookModalStepProps) {
|
||||
const [, setConditionEditorActive, conditionEditorActiveRef] =
|
||||
useStateRef(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CodeEditor
|
||||
value={webhookObject.conditions}
|
||||
minHeight={200}
|
||||
onChange={(newValue) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
conditions: newValue || "",
|
||||
});
|
||||
}}
|
||||
onValidStatusUpdate={({ isValid }) => {
|
||||
if (!conditionEditorActiveRef.current) return;
|
||||
setValidation({ ...validationRef.current!, condition: isValid });
|
||||
}}
|
||||
diagnosticsOptions={{
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: true,
|
||||
}}
|
||||
onMount={() => setConditionEditorActive(true)}
|
||||
onUnmount={() => setConditionEditorActive(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.webhooks}
|
||||
additionalVariables={additionalVariables}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
63
src/components/Table/TableHeader/Webhooks/Step3Parser.tsx
Normal file
63
src/components/Table/TableHeader/Webhooks/Step3Parser.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { IWebhookModalStepProps } from "./WebhookModal";
|
||||
import _upperFirst from "lodash/upperFirst";
|
||||
import useStateRef from "react-usestateref";
|
||||
|
||||
import CodeEditor from "@src/components/CodeEditor";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
const additionalVariables = [
|
||||
{
|
||||
key: "req",
|
||||
description: "webhook request",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Step4Body({
|
||||
webhookObject,
|
||||
setWebhookObject,
|
||||
setValidation,
|
||||
validationRef,
|
||||
}: IWebhookModalStepProps) {
|
||||
const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CodeEditor
|
||||
value={webhookObject.parser}
|
||||
minHeight={400}
|
||||
onChange={(newValue) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
parser: newValue || "",
|
||||
});
|
||||
}}
|
||||
onValidStatusUpdate={({ isValid }) => {
|
||||
if (!bodyEditorActiveRef.current) return;
|
||||
setValidation({
|
||||
...validationRef.current!,
|
||||
parser: isValid,
|
||||
});
|
||||
}}
|
||||
diagnosticsOptions={{
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: true,
|
||||
}}
|
||||
onMount={() => setBodyEditorActive(true)}
|
||||
onUnmount={() => setBodyEditorActive(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CodeEditorHelper
|
||||
docLink={
|
||||
WIKI_LINKS[`webhooks${_upperFirst(webhookObject.type)}`] ||
|
||||
WIKI_LINKS.webhooks
|
||||
}
|
||||
additionalVariables={additionalVariables}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
261
src/components/Table/TableHeader/Webhooks/WebhookList.tsx
Normal file
261
src/components/Table/TableHeader/Webhooks/WebhookList.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
|
||||
import {
|
||||
Stack,
|
||||
ButtonBase,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import WebhookIcon from "@src/assets/icons/Webhook";
|
||||
import LogsIcon from "@src/assets/icons/CloudLogs";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import { webhookTypes, webhookNames, IWebhook, WebhookType } from "./utils";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
export interface IWebhookListProps {
|
||||
webhooks: IWebhook[];
|
||||
handleAddWebhook: (type: WebhookType) => void;
|
||||
handleUpdateActive: (index: number, active: boolean) => void;
|
||||
handleOpenLogs: (index: number) => void;
|
||||
handleEdit: (index: number) => void;
|
||||
handleDelete: (index: number) => void;
|
||||
}
|
||||
|
||||
export default function WebhookList({
|
||||
webhooks,
|
||||
handleAddWebhook,
|
||||
handleUpdateActive,
|
||||
handleOpenLogs,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}: IWebhookListProps) {
|
||||
const { settings, tableState } = useProjectContext();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const addButtonRef = useRef(null);
|
||||
|
||||
const activeWebhookCount = webhooks.filter(
|
||||
(webhook) => webhook.active
|
||||
).length;
|
||||
|
||||
const handleAddButton = () => {
|
||||
setAnchorEl(addButtonRef.current);
|
||||
};
|
||||
|
||||
const handleChooseAddType = (type: WebhookType) => {
|
||||
handleClose();
|
||||
handleAddWebhook(type);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const baseUrl = `${settings?.rowyRunUrl}/whs/${tableState?.tablePath}/`;
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ marginTop: 0 }}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="h2"
|
||||
style={{ fontFeatureSettings: "'case', 'tnum'" }}
|
||||
>
|
||||
Webhooks ({activeWebhookCount} / {webhooks.length})
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddButton}
|
||||
ref={addButtonRef}
|
||||
>
|
||||
Add webhook…
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
{webhookTypes.map((type) => (
|
||||
<MenuItem onClick={() => handleChooseAddType(type)}>
|
||||
{webhookNames[type]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Stack>
|
||||
|
||||
{webhooks.length === 0 ? (
|
||||
<ButtonBase
|
||||
onClick={handleAddButton}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 72 * 3,
|
||||
borderRadius: 1,
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<EmptyState
|
||||
message="Add your first webhook"
|
||||
description="Your webhooks will appear here."
|
||||
Icon={WebhookIcon}
|
||||
/>
|
||||
</ButtonBase>
|
||||
) : (
|
||||
<List style={{ paddingTop: 0, minHeight: 72 * 3 }}>
|
||||
{webhooks.map((webhook, index) => (
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense={false}
|
||||
divider={index !== webhooks.length - 1}
|
||||
children={
|
||||
<ListItemText
|
||||
primary={
|
||||
<>
|
||||
{webhook.name} <code>{webhookNames[webhook.type]}</code>
|
||||
</>
|
||||
}
|
||||
secondary={
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 340,
|
||||
overflowX: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
<code>
|
||||
{baseUrl}
|
||||
{webhook.endpoint}
|
||||
</code>
|
||||
</Typography>
|
||||
</div>
|
||||
<Tooltip title="copy to clipboard">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`${baseUrl}${webhook.endpoint}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
//secondary={webhookNames[webhook.type]}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
minHeight: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
secondaryAction={
|
||||
<Stack alignItems="flex-end">
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip title={webhook.active ? "Deactivate" : "Activate"}>
|
||||
<Switch
|
||||
checked={webhook.active}
|
||||
onClick={() =>
|
||||
handleUpdateActive(index, !webhook.active)
|
||||
}
|
||||
inputProps={{ "aria-label": "Activate" }}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Logs">
|
||||
<IconButton
|
||||
aria-label="Logs"
|
||||
onClick={() => handleOpenLogs(index)}
|
||||
>
|
||||
<LogsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
aria-label="Edit"
|
||||
onClick={() => handleEdit(index)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
aria-label="Delete"
|
||||
color="error"
|
||||
onClick={() => handleDelete(index)}
|
||||
sx={{ "&&": { mr: -1.5 } }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Last updated
|
||||
<br />
|
||||
by {webhook.lastEditor.displayName}
|
||||
<br />
|
||||
at{" "}
|
||||
{format(
|
||||
webhook.lastEditor.lastUpdate,
|
||||
DATE_TIME_FORMAT
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.disabled" }}
|
||||
>
|
||||
{formatRelative(
|
||||
webhook.lastEditor.lastUpdate,
|
||||
new Date()
|
||||
)}
|
||||
</Typography>
|
||||
<Avatar
|
||||
alt={`${webhook.lastEditor.displayName}’s profile photo`}
|
||||
src={webhook.lastEditor.photoURL}
|
||||
sx={{ width: 24, height: 24, "&&": { mr: -0.5 } }}
|
||||
/>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
74
src/components/Table/TableHeader/Webhooks/WebhookLogs.tsx
Normal file
74
src/components/Table/TableHeader/Webhooks/WebhookLogs.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import _isEqual from "lodash/isEqual";
|
||||
import _upperFirst from "lodash/upperFirst";
|
||||
|
||||
import Modal, { IModalProps } from "@src/components/Modal";
|
||||
|
||||
import { IWebhook } from "./utils";
|
||||
import useCollection from "@src/hooks/useCollection";
|
||||
import { useEffect } from "react";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { Typography } from "@mui/material";
|
||||
import { orderBy } from "lodash";
|
||||
|
||||
export interface IWebhookLogsProps {
|
||||
handleClose: IModalProps["onClose"];
|
||||
webhookObject: IWebhook;
|
||||
}
|
||||
|
||||
export default function WebhookModal({
|
||||
handleClose,
|
||||
webhookObject,
|
||||
}: IWebhookLogsProps) {
|
||||
const { tableState } = useProjectContext();
|
||||
const [logsCollection, logsDispatch] = useCollection({});
|
||||
useEffect(() => {
|
||||
if (webhookObject && tableState?.tablePath) {
|
||||
logsDispatch({
|
||||
path: "_rowy_/webhooks/logs",
|
||||
filters: [
|
||||
{
|
||||
field: "params.endpoint",
|
||||
operator: "==",
|
||||
value: webhookObject.endpoint,
|
||||
},
|
||||
{
|
||||
field: "params.tablePath",
|
||||
operator: "==",
|
||||
value: tableState?.tablePath,
|
||||
},
|
||||
],
|
||||
orderBy: { key: "createdAt", direction: "desc" },
|
||||
limit: 50,
|
||||
});
|
||||
}
|
||||
}, [webhookObject, tableState?.tablePath]);
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
fullWidth
|
||||
title={`Webhook logs: ${webhookObject.name}`}
|
||||
sx={{
|
||||
"& .MuiPaper-root": {
|
||||
maxWidth: 742 + 20,
|
||||
height: 980,
|
||||
},
|
||||
}}
|
||||
children={
|
||||
<>
|
||||
{logsCollection.documents.map((doc) => (
|
||||
<Typography>{`${doc.createdAt.toDate()} - ${
|
||||
doc.response
|
||||
}`}</Typography>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
255
src/components/Table/TableHeader/Webhooks/WebhookModal.tsx
Normal file
255
src/components/Table/TableHeader/Webhooks/WebhookModal.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useState } from "react";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
import _upperFirst from "lodash/upperFirst";
|
||||
import useStateRef from "react-usestateref";
|
||||
|
||||
import {
|
||||
Grid,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Stepper,
|
||||
Step,
|
||||
StepButton,
|
||||
StepContent,
|
||||
Typography,
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import Modal, { IModalProps } from "@src/components/Modal";
|
||||
import Step1Secret from "./Step1Secret";
|
||||
import Step2Conditions from "./Step2Conditions";
|
||||
import Step3Body from "./Step3Parser";
|
||||
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
import { webhookNames, IWebhook } from "./utils";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
type StepValidation = Record<"condition" | "parser", boolean>;
|
||||
export interface IWebhookModalStepProps {
|
||||
webhookObject: IWebhook;
|
||||
setWebhookObject: React.Dispatch<React.SetStateAction<IWebhook>>;
|
||||
validation: StepValidation;
|
||||
setValidation: React.Dispatch<React.SetStateAction<StepValidation>>;
|
||||
validationRef: React.RefObject<StepValidation>;
|
||||
}
|
||||
|
||||
export interface IWebhookModalProps {
|
||||
handleClose: IModalProps["onClose"];
|
||||
handleAdd: (webhookObject: IWebhook) => void;
|
||||
handleUpdate: (webhookObject: IWebhook) => void;
|
||||
mode: "add" | "update";
|
||||
webhookObject: IWebhook;
|
||||
}
|
||||
|
||||
export default function WebhookModal({
|
||||
handleClose,
|
||||
handleAdd,
|
||||
handleUpdate,
|
||||
mode,
|
||||
webhookObject: initialObject,
|
||||
}: IWebhookModalProps) {
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [webhookObject, setWebhookObject] = useState<IWebhook>(initialObject);
|
||||
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
|
||||
const [validation, setValidation, validationRef] =
|
||||
useStateRef<StepValidation>({ condition: true, parser: true });
|
||||
|
||||
const edited = !_isEqual(initialObject, webhookObject);
|
||||
|
||||
const handleAddOrUpdate = () => {
|
||||
if (mode === "add") handleAdd(webhookObject);
|
||||
if (mode === "update") handleUpdate(webhookObject);
|
||||
};
|
||||
|
||||
const stepProps = {
|
||||
webhookObject,
|
||||
setWebhookObject,
|
||||
validation,
|
||||
setValidation,
|
||||
validationRef,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
fullWidth
|
||||
title={`${mode === "add" ? "Add" : "Update"} webhook: ${
|
||||
webhookNames[webhookObject.type]
|
||||
}`}
|
||||
sx={{
|
||||
"& .MuiPaper-root": {
|
||||
maxWidth: 742 + 20,
|
||||
height: 980,
|
||||
},
|
||||
}}
|
||||
children={
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
size="small"
|
||||
required
|
||||
label="Webhook name"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
autoFocus
|
||||
value={webhookObject.name}
|
||||
error={edited && !webhookObject.name.length}
|
||||
helperText={
|
||||
edited && !webhookObject.name.length ? "Required" : " "
|
||||
}
|
||||
onChange={(event) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
name: event.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={webhookObject.active}
|
||||
onChange={(e) =>
|
||||
setWebhookObject((webhookObject) => ({
|
||||
...webhookObject,
|
||||
active: e.target.checked,
|
||||
}))
|
||||
}
|
||||
size="medium"
|
||||
/>
|
||||
}
|
||||
label={`Webhook endpoint is ${
|
||||
!webhookObject.active ? "de" : ""
|
||||
}activated`}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Stepper
|
||||
nonLinear
|
||||
activeStep={activeStep}
|
||||
orientation="vertical"
|
||||
sx={{
|
||||
mt: 0,
|
||||
|
||||
"& .MuiStepLabel-root": { width: "100%" },
|
||||
"& .MuiStepLabel-label": {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
typography: "subtitle2",
|
||||
"&.Mui-active": { typography: "subtitle2" },
|
||||
},
|
||||
"& .MuiStepLabel-label svg": {
|
||||
display: "block",
|
||||
marginLeft: "auto",
|
||||
my: ((24 - 18) / 2 / 8) * -1,
|
||||
transition: (theme) => theme.transitions.create("transform"),
|
||||
},
|
||||
"& .Mui-active svg": {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Step>
|
||||
<StepButton onClick={() => setActiveStep(0)}>
|
||||
Verification
|
||||
<ExpandIcon />
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Typography gutterBottom>
|
||||
Set the verification secret for the webhook.
|
||||
</Typography>
|
||||
<Step1Secret {...stepProps} />
|
||||
</StepContent>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
<StepButton onClick={() => setActiveStep(1)}>
|
||||
Conditions (optional)
|
||||
<ExpandIcon />
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Typography gutterBottom>
|
||||
Optionally, write a function that determines if the webhook
|
||||
call should be processed. Leave the function to always return{" "}
|
||||
<code>true</code> if you do not want to write additional
|
||||
logic.
|
||||
</Typography>
|
||||
<Step2Conditions {...stepProps} />
|
||||
</StepContent>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
<StepButton onClick={() => setActiveStep(2)}>
|
||||
Parser
|
||||
<ExpandIcon />
|
||||
</StepButton>
|
||||
<StepContent>
|
||||
<Typography gutterBottom>
|
||||
Write the webhook parsed function. The returned object of the
|
||||
parser will be added as new row{" "}
|
||||
<Link
|
||||
href={
|
||||
WIKI_LINKS[
|
||||
`webhooks${_upperFirst(webhookObject.type)}`
|
||||
] || WIKI_LINKS.webhooks
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Docs
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</Typography>
|
||||
<Step3Body {...stepProps} />
|
||||
</StepContent>
|
||||
</Step>
|
||||
</Stepper>
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
children: mode === "add" ? "Add" : "Update",
|
||||
disabled: !edited || !webhookObject.name.length,
|
||||
onClick: () => {
|
||||
let warningMessage;
|
||||
if (!validation.condition && !validation.parser) {
|
||||
warningMessage = "Condition and webhook body are not valid";
|
||||
} else if (!validation.condition) {
|
||||
warningMessage = "Condition is not valid";
|
||||
} else if (!validation.parser) {
|
||||
warningMessage = "Webhook body is not valid";
|
||||
}
|
||||
if (warningMessage) {
|
||||
requestConfirmation({
|
||||
title: "Validation failed",
|
||||
body: `${warningMessage}. Continue?`,
|
||||
confirm: "Yes, I know what I’m doing",
|
||||
cancel: "No, I’ll fix the errors",
|
||||
handleConfirm: handleAddOrUpdate,
|
||||
});
|
||||
} else {
|
||||
handleAddOrUpdate();
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
244
src/components/Table/TableHeader/Webhooks/index.tsx
Normal file
244
src/components/Table/TableHeader/Webhooks/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { useState } from "react";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
|
||||
import { Breadcrumbs } from "@mui/material";
|
||||
|
||||
import TableHeaderButton from "../TableHeaderButton";
|
||||
import WebhookIcon from "@src/assets/icons/Webhook";
|
||||
import Modal from "@src/components/Modal";
|
||||
import WebhookList from "./WebhookList";
|
||||
import WebhookModal from "./WebhookModal";
|
||||
import WebhookLogs from "./WebhookLogs";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
import { emptyWebhookObject, IWebhook, WebhookType } from "./utils";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { analytics } from "@src/analytics";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
export default function Webhooks() {
|
||||
const { tableState, tableActions, rowyRun, compatibleRowyRunVersion } =
|
||||
useProjectContext();
|
||||
const appContext = useAppContext();
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const currentWebhooks = (tableState?.config.webhooks ?? []) as IWebhook[];
|
||||
const [localWebhooksObjects, setLocalWebhooksObjects] =
|
||||
useState(currentWebhooks);
|
||||
const [openWebhookList, setOpenWebhookList] = useState(false);
|
||||
const [webhookModal, setWebhookModal] = useState<{
|
||||
mode: "add" | "update";
|
||||
webhookObject: IWebhook;
|
||||
index?: number;
|
||||
} | null>(null);
|
||||
const [webhookLogs, setWebhookLogs] = useState<IWebhook | null>();
|
||||
if (!compatibleRowyRunVersion?.({ minVersion: "1.1.1" })) return <></>;
|
||||
const edited = !_isEqual(currentWebhooks, localWebhooksObjects);
|
||||
|
||||
const tablePathTokens =
|
||||
tableState?.tablePath?.split("/").filter(function (_, i) {
|
||||
// replace IDs with dash that appears at even indexes
|
||||
return i % 2 === 0;
|
||||
}) ?? [];
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpenWebhookList(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (edited) {
|
||||
requestConfirmation({
|
||||
title: "Discard changes",
|
||||
body: "You will lose changes you have made to webhooks",
|
||||
confirm: "Discard",
|
||||
handleConfirm: () => {
|
||||
setLocalWebhooksObjects(currentWebhooks);
|
||||
setOpenWebhookList(false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setOpenWebhookList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveWebhooks = async () => {
|
||||
tableActions?.table.updateConfig("webhooks", localWebhooksObjects);
|
||||
setOpenWebhookList(false);
|
||||
// TODO: convert to async function that awaits for the document write to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
};
|
||||
|
||||
const handleSaveDeploy = async () => {
|
||||
await handleSaveWebhooks();
|
||||
try {
|
||||
if (rowyRun) {
|
||||
const resp = await rowyRun({
|
||||
route: runRoutes.publishWebhooks,
|
||||
body: {
|
||||
tableConfigPath: tableState?.config.tableConfig.path,
|
||||
tablePath: tableState?.tablePath,
|
||||
},
|
||||
});
|
||||
enqueueSnackbar(resp.message, {
|
||||
variant: resp.success ? "success" : "error",
|
||||
});
|
||||
|
||||
analytics.logEvent("published_webhooks");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddWebhook = (webhookObject: IWebhook) => {
|
||||
setLocalWebhooksObjects([...localWebhooksObjects, webhookObject]);
|
||||
analytics.logEvent("created_webhook", { type: webhookObject.type });
|
||||
setWebhookModal(null);
|
||||
};
|
||||
|
||||
const handleUpdateWebhook = (webhookObject: IWebhook) => {
|
||||
setLocalWebhooksObjects(
|
||||
localWebhooksObjects.map((webhook, index) => {
|
||||
if (index === webhookModal?.index) {
|
||||
return {
|
||||
...webhookObject,
|
||||
lastEditor: currentEditor(),
|
||||
};
|
||||
} else {
|
||||
return webhook;
|
||||
}
|
||||
})
|
||||
);
|
||||
analytics.logEvent("updated_webhook", { type: webhookObject.type });
|
||||
setWebhookModal(null);
|
||||
};
|
||||
|
||||
const handleUpdateActive = (index: number, active: boolean) => {
|
||||
setLocalWebhooksObjects(
|
||||
localWebhooksObjects.map((webhookObject, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...webhookObject,
|
||||
active,
|
||||
lastEditor: currentEditor(),
|
||||
};
|
||||
} else {
|
||||
return webhookObject;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleOpenLogs = (index: number) => {
|
||||
const _webhook = localWebhooksObjects[index];
|
||||
|
||||
setWebhookLogs(_webhook);
|
||||
analytics.logEvent("view_webhook_logs", {
|
||||
type: _webhook.type,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (index: number) => {
|
||||
setWebhookModal({
|
||||
mode: "update",
|
||||
webhookObject: localWebhooksObjects[index],
|
||||
index,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
requestConfirmation({
|
||||
title: `Delete ${localWebhooksObjects[index].name}?`,
|
||||
body: "This webhook will be permanently deleted.",
|
||||
confirm: "Confirm",
|
||||
handleConfirm: () => {
|
||||
setLocalWebhooksObjects(
|
||||
localWebhooksObjects.filter((_, i) => i !== index)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const currentEditor = () => ({
|
||||
displayName: appContext?.currentUser?.displayName ?? "Unknown user",
|
||||
photoURL: appContext?.currentUser?.photoURL ?? "",
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderButton
|
||||
title="Webhooks"
|
||||
onClick={handleOpen}
|
||||
icon={<WebhookIcon />}
|
||||
/>
|
||||
|
||||
{openWebhookList && !!tableState && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title="Webhooks"
|
||||
children={
|
||||
<>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
{tablePathTokens.map((pathToken) => (
|
||||
<code>{pathToken}</code>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<WebhookList
|
||||
webhooks={localWebhooksObjects}
|
||||
handleAddWebhook={(type: WebhookType) => {
|
||||
setWebhookModal({
|
||||
mode: "add",
|
||||
webhookObject: emptyWebhookObject(type, currentEditor()),
|
||||
});
|
||||
}}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleOpenLogs={handleOpenLogs}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
children: "Save & Deploy",
|
||||
onClick: handleSaveDeploy,
|
||||
disabled: !edited,
|
||||
},
|
||||
secondary: {
|
||||
children: "Save",
|
||||
onClick: handleSaveWebhooks,
|
||||
disabled: !edited,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{webhookModal && (
|
||||
<WebhookModal
|
||||
handleClose={() => {
|
||||
setWebhookModal(null);
|
||||
}}
|
||||
handleAdd={handleAddWebhook}
|
||||
handleUpdate={handleUpdateWebhook}
|
||||
mode={webhookModal.mode}
|
||||
webhookObject={webhookModal.webhookObject}
|
||||
/>
|
||||
)}
|
||||
{webhookLogs && (
|
||||
<WebhookLogs
|
||||
webhookObject={webhookLogs}
|
||||
handleClose={() => {
|
||||
setWebhookLogs(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
105
src/components/Table/TableHeader/Webhooks/utils.ts
Normal file
105
src/components/Table/TableHeader/Webhooks/utils.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { generateRandomId } from "@src/utils/fns";
|
||||
|
||||
export const webhookTypes = [
|
||||
"basic",
|
||||
"typeform",
|
||||
"sendgrid",
|
||||
"shopify",
|
||||
"twitter",
|
||||
"stripe",
|
||||
] as const;
|
||||
|
||||
export type WebhookType = typeof webhookTypes[number];
|
||||
|
||||
export const webhookNames: Record<WebhookType, string> = {
|
||||
sendgrid: "Sendgrid",
|
||||
typeform: "Typeform",
|
||||
shopify: "Shopify",
|
||||
twitter: "Twitter",
|
||||
stripe: "Stripe",
|
||||
basic: "Basic",
|
||||
};
|
||||
|
||||
export interface IWebhookEditor {
|
||||
displayName: string;
|
||||
photoURL: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface IWebhook {
|
||||
// rowy meta fields
|
||||
name: string;
|
||||
active: boolean;
|
||||
lastEditor: IWebhookEditor;
|
||||
// webhook specific fields
|
||||
endpoint: string;
|
||||
type: WebhookType;
|
||||
parser: string;
|
||||
conditions: string;
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
const parserTemplates = {
|
||||
basic: `const basicParser: BasicParser = 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;
|
||||
return body;
|
||||
}`,
|
||||
typeform: `const typeformParser: TypeformParser = async({req, db,ref}) =>{
|
||||
// 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
|
||||
// set the ref value to field key you would like to sync to
|
||||
// docs: https://help.typeform.com/hc/en-us/articles/360050447552-Block-reference-format-restrictions
|
||||
const {submitted_at,hidden,answers} = req.body.form_response
|
||||
return ({
|
||||
_createdAt: submitted_at,
|
||||
...hidden,
|
||||
...answers.reduce((accRow, currAnswer) => {
|
||||
switch (currAnswer.type) {
|
||||
case "date":
|
||||
return {
|
||||
...accRow,
|
||||
[currAnswer.field.ref]: new Date(currAnswer[currAnswer.type]),
|
||||
};
|
||||
case "choice":
|
||||
return {
|
||||
...accRow,
|
||||
[currAnswer.field.ref]: currAnswer[currAnswer.type].label,
|
||||
};
|
||||
case "choices":
|
||||
return {
|
||||
...accRow,
|
||||
[currAnswer.field.ref]: currAnswer[currAnswer.type].labels,
|
||||
};
|
||||
case "file_url":
|
||||
default:
|
||||
return {
|
||||
...accRow,
|
||||
[currAnswer.field.ref]: currAnswer[currAnswer.type],
|
||||
};
|
||||
}
|
||||
}, {}),
|
||||
})};`,
|
||||
};
|
||||
export function emptyWebhookObject(
|
||||
type: WebhookType,
|
||||
user: IWebhookEditor
|
||||
): IWebhook {
|
||||
return {
|
||||
name: "Untitled webhook",
|
||||
active: false,
|
||||
endpoint: generateRandomId(),
|
||||
type,
|
||||
parser: parserTemplates[type] ?? parserTemplates["basic"],
|
||||
conditions: `const condition: Condition = async({ref,req,db}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
lastEditor: user,
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import CloudLogs from "./CloudLogs";
|
||||
import HiddenFields from "../HiddenFields";
|
||||
import RowHeight from "./RowHeight";
|
||||
import Extensions from "./Extensions";
|
||||
import Webhooks from "./Webhooks";
|
||||
import ReExecute from "./ReExecute";
|
||||
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
@@ -99,6 +100,7 @@ export default function TableHeader() {
|
||||
{userClaims?.roles?.includes("ADMIN") && (
|
||||
<>
|
||||
{/* Spacer */} <div />
|
||||
<Webhooks />
|
||||
<Extensions />
|
||||
<CloudLogs />
|
||||
<TableLogs />
|
||||
|
||||
@@ -28,11 +28,11 @@ export default function FinalColumn({ row }: FormatterProps<any, any>) {
|
||||
useStyles();
|
||||
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { tableActions, addRow } = useProjectContext();
|
||||
const { deleteRow, addRow } = useProjectContext();
|
||||
const altPress = useKeyPress("Alt");
|
||||
|
||||
const handleDelete = () => tableActions!.row.delete(row.id);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (deleteRow) deleteRow(row.id);
|
||||
};
|
||||
return (
|
||||
<Stack direction="row" spacing={0.5}>
|
||||
<Tooltip title="Duplicate row">
|
||||
@@ -48,7 +48,7 @@ export default function FinalColumn({ row }: FormatterProps<any, any>) {
|
||||
Object.keys(clonedRow).forEach((key) => {
|
||||
if (clonedRow[key] === undefined) delete clonedRow[key];
|
||||
});
|
||||
if (tableActions) addRow!(clonedRow);
|
||||
if (addRow) addRow!(clonedRow);
|
||||
}}
|
||||
aria-label="Duplicate row"
|
||||
className="row-hover-iconButton"
|
||||
@@ -61,7 +61,6 @@ export default function FinalColumn({ row }: FormatterProps<any, any>) {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
disabled={!tableActions}
|
||||
onClick={
|
||||
altPress
|
||||
? handleDelete
|
||||
|
||||
@@ -48,6 +48,7 @@ const WIKI_PATHS = {
|
||||
extensionsSlackMessage: "/extensions/slack-message",
|
||||
extensionsSendgridEmail: "/extensions/sendgrid-email",
|
||||
extensionsTwilioMessage: "/extensions/twilio-message",
|
||||
webhooks: "/webhooks",
|
||||
};
|
||||
export const WIKI_LINKS = _mapValues(
|
||||
WIKI_PATHS,
|
||||
|
||||
@@ -44,6 +44,7 @@ export const runRoutes = {
|
||||
migrateFT2Rowy: { path: "/migrateFT2Rowy", method: "GET" } as RunRoute,
|
||||
actionScript: { path: "/actionScript", method: "POST" } as RunRoute,
|
||||
buildFunction: { path: "/buildFunction", method: "POST" } as RunRoute,
|
||||
publishWebhooks: { path: "/publishWebhooks", method: "POST" } as RunRoute,
|
||||
projectOwner: { path: "/projectOwner", method: "GET" } as RunRoute,
|
||||
setOwnerRoles: { path: "/setOwnerRoles", method: "GET" } as RunRoute,
|
||||
inviteUser: { path: "/inviteUser", method: "POST" } as RunRoute,
|
||||
@@ -51,4 +52,6 @@ export const runRoutes = {
|
||||
deleteUser: { path: "/deleteUser", method: "DELETE" } as RunRoute,
|
||||
algoliaSearchKey: { path: `/algoliaSearchKey`, method: "GET" } as RunRoute,
|
||||
algoliaAppId: { path: `/algoliaAppId`, method: "GET" } as RunRoute,
|
||||
functionLogs: { path: `/functionLogs`, method: "GET" } as RunRoute,
|
||||
auditChange: { path: `/auditChange`, method: "POST" } as RunRoute,
|
||||
} as const;
|
||||
|
||||
@@ -16,10 +16,10 @@ import { ColumnMenuRef } from "@src/components/Table/ColumnMenu";
|
||||
import { ImportWizardRef } from "@src/components/Wizards/ImportWizard";
|
||||
|
||||
import { rowyRun, IRowyRunRequestProps } from "@src/utils/rowyRun";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { rowyUser } from "@src/utils/fns";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import semver from "semver";
|
||||
export type Table = {
|
||||
id: string;
|
||||
collection: string;
|
||||
@@ -34,12 +34,16 @@ export type Table = {
|
||||
};
|
||||
|
||||
interface IProjectContext {
|
||||
settings: {
|
||||
rowyRunUrl?: string;
|
||||
};
|
||||
tables: Table[];
|
||||
table: Table;
|
||||
roles: string[];
|
||||
tableState: TableState;
|
||||
tableActions: TableActions;
|
||||
addRow: (data?: Record<string, any>, ignoreRequiredFields?: boolean) => void;
|
||||
deleteRow: (rowId) => void;
|
||||
updateCell: (
|
||||
ref: firebase.firestore.DocumentReference,
|
||||
fieldName: string,
|
||||
@@ -71,6 +75,10 @@ interface IProjectContext {
|
||||
deleteTable: (id: string) => void;
|
||||
};
|
||||
|
||||
compatibleRowyRunVersion: (args: {
|
||||
minVersion?: string;
|
||||
maxVersion?: string;
|
||||
}) => boolean;
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
dataGridRef: React.RefObject<DataGridHandle>;
|
||||
// A ref to the side drawer state. Prevents unnecessary re-renders
|
||||
@@ -99,6 +107,17 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
const [settings, settingsActions] = useSettings();
|
||||
const table = _find(tables, (table) => table.id === tableState.config.id);
|
||||
|
||||
const [rowyRunVersion, setRowyRunVersion] = useState("");
|
||||
useEffect(() => {
|
||||
if (settings?.doc?.rowyRunUrl) {
|
||||
_rowyRun({
|
||||
route: runRoutes.version,
|
||||
}).then((resp) => {
|
||||
if (resp.version) setRowyRunVersion(resp.version);
|
||||
});
|
||||
}
|
||||
}, [settings?.doc?.rowyRunUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const { tables } = settings;
|
||||
if (tables && userRoles) {
|
||||
@@ -133,7 +152,31 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
: [],
|
||||
[tables]
|
||||
);
|
||||
|
||||
const auditChange = (
|
||||
type: "ADD_ROW" | "UPDATE_CELL" | "DELETE_ROW",
|
||||
rowId,
|
||||
data
|
||||
) => {
|
||||
if (
|
||||
table?.audit !== false &&
|
||||
compatibleRowyRunVersion({ minVersion: "1.1.1" })
|
||||
) {
|
||||
_rowyRun({
|
||||
route: runRoutes.auditChange,
|
||||
body: {
|
||||
rowyUser: rowyUser(currentUser!),
|
||||
type,
|
||||
ref: {
|
||||
rowPath: tableState.tablePath,
|
||||
rowId,
|
||||
tableId: table?.id,
|
||||
collectionPath: tableState.tablePath,
|
||||
},
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const addRow: IProjectContext["addRow"] = (data, ignoreRequiredFields) => {
|
||||
const valuesFromFilter = tableState.filters.reduce((acc, curr) => {
|
||||
if (curr.operator === "==") {
|
||||
@@ -170,7 +213,8 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
|
||||
tableActions.row.add(
|
||||
{ ...valuesFromFilter, ...initialData, ...data },
|
||||
ignoreRequiredFields ? [] : requiredFields
|
||||
ignoreRequiredFields ? [] : requiredFields,
|
||||
(rowId: string) => auditChange("ADD_ROW", rowId, {})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -190,11 +234,11 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
{ updatedField: fieldName }
|
||||
);
|
||||
}
|
||||
|
||||
tableActions.row.update(
|
||||
ref,
|
||||
update,
|
||||
() => {
|
||||
auditChange("UPDATE_CELL", ref.id, { updatedField: fieldName });
|
||||
if (onSuccess) onSuccess(ref, fieldName, value);
|
||||
},
|
||||
(error) => {
|
||||
@@ -210,6 +254,10 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const deleteRow = (rowId) => {
|
||||
tableActions.row.delete(rowId, () => auditChange("DELETE_ROW", rowId, {}));
|
||||
};
|
||||
// rowyRun access
|
||||
const _rowyRun: IProjectContext["rowyRun"] = async (args) => {
|
||||
const authToken = await getAuthToken();
|
||||
@@ -237,6 +285,20 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const compatibleRowyRunVersion = ({
|
||||
minVersion,
|
||||
maxVersion,
|
||||
}: {
|
||||
minVersion?: string;
|
||||
maxVersion?: string;
|
||||
}) => {
|
||||
// example: "1.0.0", "1.0.0-beta.1", "1.0.0-rc.1+1"
|
||||
const version = rowyRunVersion.split("-")[0];
|
||||
if (!version) return false;
|
||||
if (minVersion && semver.lt(version, minVersion)) return false;
|
||||
if (maxVersion && semver.gt(version, maxVersion)) return false;
|
||||
return true;
|
||||
};
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
const dataGridRef = useRef<DataGridHandle>(null);
|
||||
const sideDrawerRef = useRef<SideDrawerRef>();
|
||||
@@ -250,7 +312,9 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
tableActions,
|
||||
addRow,
|
||||
updateCell,
|
||||
deleteRow,
|
||||
settingsActions,
|
||||
settings: settings.doc,
|
||||
roles,
|
||||
tables,
|
||||
table,
|
||||
@@ -259,6 +323,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
columnMenuRef,
|
||||
importWizardRef,
|
||||
rowyRun: _rowyRun,
|
||||
compatibleRowyRunVersion,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -7,14 +7,6 @@ import githubLogo from "@src/assets/logos/github.svg";
|
||||
import appleLogo from "@src/assets/logos/apple.svg";
|
||||
import yahooLogo from "@src/assets/logos/yahoo.svg";
|
||||
|
||||
import { mdiGoogle } from "@mdi/js";
|
||||
console.log(
|
||||
`data:image/svg+xml;utf8,` +
|
||||
encodeURIComponent(
|
||||
`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="${mdiGoogle}" /></svg>`
|
||||
)
|
||||
);
|
||||
|
||||
export const authOptions = {
|
||||
google: {
|
||||
provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID,
|
||||
|
||||
@@ -26,10 +26,10 @@ export type TableState = {
|
||||
id: string;
|
||||
rowHeight: number;
|
||||
tableConfig: any;
|
||||
webhooks: any;
|
||||
sparks: string;
|
||||
compiledExtension: string;
|
||||
extensionObjects?: any[];
|
||||
webhooks?: any[];
|
||||
functionConfigPath?: string;
|
||||
};
|
||||
columns: any[];
|
||||
|
||||
@@ -231,12 +231,12 @@ const useTableData = () => {
|
||||
* @param rowIndex local position
|
||||
* @param documentId firestore document id
|
||||
*/
|
||||
const deleteRow = (rowId: string) => {
|
||||
const deleteRow = async (rowId: string, onSuccess: () => void) => {
|
||||
// Remove row locally
|
||||
rowsDispatch({ type: "delete", rowId });
|
||||
// Delete document
|
||||
try {
|
||||
db.collection(tableState.path).doc(rowId).delete();
|
||||
await db.collection(tableState.path).doc(rowId).delete().then(onSuccess);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
if (error.code === "permission-denied") {
|
||||
@@ -264,7 +264,11 @@ const useTableData = () => {
|
||||
/** creating new document/row
|
||||
* @param data(optional: default will create empty row)
|
||||
*/
|
||||
const addRow = async (data: any, requiredFields: string[]) => {
|
||||
const addRow = (
|
||||
data: any,
|
||||
requiredFields: string[],
|
||||
onSuccess: (rowId: string) => void
|
||||
) => {
|
||||
const missingRequiredFields = requiredFields
|
||||
? requiredFields.reduce(missingFieldsReducer(data), [])
|
||||
: [];
|
||||
@@ -274,7 +278,12 @@ const useTableData = () => {
|
||||
|
||||
if (missingRequiredFields.length === 0) {
|
||||
try {
|
||||
await db.collection(path).doc(newId).set(data, { merge: true });
|
||||
db.collection(path)
|
||||
.doc(newId)
|
||||
.set(data, { merge: true })
|
||||
.then(() => {
|
||||
onSuccess(newId);
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.code === "permission-denied") {
|
||||
enqueueSnackbar("You do not have the permissions to add new rows.", {
|
||||
|
||||
@@ -199,6 +199,12 @@ export const rowyUser = (
|
||||
...data,
|
||||
};
|
||||
};
|
||||
export const generateRandomId = () => {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
};
|
||||
|
||||
const _firestoreRefSanitizer = (v: any) => {
|
||||
// If react-hook-form receives a Firestore document reference, it tries to
|
||||
|
||||
Reference in New Issue
Block a user