mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
fix webhooks, extensions styling
This commit is contained in:
@@ -62,7 +62,6 @@
|
||||
"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",
|
||||
|
||||
83
src/components/TableHeader/Extensions/AddExtensionButton.tsx
Normal file
83
src/components/TableHeader/Extensions/AddExtensionButton.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import EmailIcon from "@mui/icons-material/EmailOutlined";
|
||||
|
||||
import { extensionTypes, extensionNames, ExtensionType } from "./utils";
|
||||
import { EMAIL_REQUEST } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IAddExtensionButtonProps extends Partial<ButtonProps> {
|
||||
handleAddExtension: (type: ExtensionType) => void;
|
||||
}
|
||||
|
||||
export default function AddExtensionButton({
|
||||
handleAddExtension,
|
||||
...props
|
||||
}: IAddExtensionButtonProps) {
|
||||
const addButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleChooseAddType = (type: ExtensionType) => {
|
||||
setOpen(false);
|
||||
handleAddExtension(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
ref={addButtonRef}
|
||||
sx={{
|
||||
alignSelf: { sm: "flex-end" },
|
||||
mt: {
|
||||
sm: `calc(var(--dialog-title-height) * -1 + (var(--dialog-title-height) - 32px) / 2)`,
|
||||
},
|
||||
mx: { xs: "var(--dialog-spacing)", sm: undefined },
|
||||
mr: { sm: 8 },
|
||||
mb: { xs: 1.5, sm: 2 },
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Add Extension…
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
anchorEl={addButtonRef.current}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
{extensionTypes.map((type) => (
|
||||
<MenuItem onClick={() => handleChooseAddType(type)}>
|
||||
{extensionNames[type]}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EMAIL_REQUEST}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EmailIcon aria-label="Send email" />
|
||||
</ListItemIcon>
|
||||
Request new Extension…
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,27 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
|
||||
import {
|
||||
Stack,
|
||||
ButtonBase,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import ExtensionIcon from "@src/assets/icons/Extension";
|
||||
import DuplicateIcon from "@src/assets/icons/Copy";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import EmailIcon from "@mui/icons-material/EmailOutlined";
|
||||
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import {
|
||||
extensionTypes,
|
||||
extensionNames,
|
||||
IExtension,
|
||||
ExtensionType,
|
||||
} from "./utils";
|
||||
import { extensionNames, IExtension } from "./utils";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { EMAIL_REQUEST } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IExtensionListProps {
|
||||
extensions: IExtension[];
|
||||
handleAddExtension: (type: ExtensionType) => void;
|
||||
handleUpdateActive: (index: number, active: boolean) => void;
|
||||
handleDuplicate: (index: number) => void;
|
||||
handleEdit: (index: number) => void;
|
||||
@@ -46,204 +30,127 @@ export interface IExtensionListProps {
|
||||
|
||||
export default function ExtensionList({
|
||||
extensions,
|
||||
handleAddExtension,
|
||||
handleUpdateActive,
|
||||
handleDuplicate,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}: IExtensionListProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const addButtonRef = useRef(null);
|
||||
|
||||
const activeExtensionCount = extensions.filter(
|
||||
(extension) => extension.active
|
||||
).length;
|
||||
|
||||
const handleAddButton = () => {
|
||||
setAnchorEl(addButtonRef.current);
|
||||
};
|
||||
|
||||
const handleChooseAddType = (type: ExtensionType) => {
|
||||
handleClose();
|
||||
handleAddExtension(type);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
if (extensions.length === 0)
|
||||
return (
|
||||
<EmptyState
|
||||
message="Add your first extension above"
|
||||
description="Your extensions will appear here"
|
||||
Icon={ExtensionIcon}
|
||||
style={{ height: 89 * 3 - 1 }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
style={{ marginTop: 0 }}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="h2"
|
||||
style={{ fontFeatureSettings: "'case', 'tnum'" }}
|
||||
>
|
||||
Extensions ({activeExtensionCount} / {extensions.length})
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleAddButton}
|
||||
ref={addButtonRef}
|
||||
>
|
||||
Add Extension…
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
{extensionTypes.map((type) => (
|
||||
<MenuItem onClick={() => handleChooseAddType(type)}>
|
||||
{extensionNames[type]}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EMAIL_REQUEST}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EmailIcon aria-label="Send email" />
|
||||
</ListItemIcon>
|
||||
Request new Extension…
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
|
||||
{extensions.length === 0 ? (
|
||||
<ButtonBase
|
||||
onClick={handleAddButton}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 72 * 3,
|
||||
borderRadius: 1,
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
}}
|
||||
>
|
||||
<EmptyState
|
||||
message="Add your first extension"
|
||||
description="Your extensions will appear here."
|
||||
Icon={ExtensionIcon}
|
||||
/>
|
||||
</ButtonBase>
|
||||
) : (
|
||||
<List style={{ paddingTop: 0, minHeight: 72 * 3 }}>
|
||||
{extensions.map((extensionObject, index) => (
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense={false}
|
||||
divider={index !== extensions.length - 1}
|
||||
children={
|
||||
<ListItemText
|
||||
primary={extensionObject.name}
|
||||
secondary={extensionNames[extensionObject.type]}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
minHeight: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
secondaryAction={
|
||||
<Stack alignItems="flex-end">
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip
|
||||
title={extensionObject.active ? "Deactivate" : "Activate"}
|
||||
>
|
||||
<Switch
|
||||
checked={extensionObject.active}
|
||||
onClick={() =>
|
||||
handleUpdateActive(index, !extensionObject.active)
|
||||
}
|
||||
inputProps={{ "aria-label": "Activate" }}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Duplicate">
|
||||
<IconButton
|
||||
aria-label="Duplicate"
|
||||
onClick={() => handleDuplicate(index)}
|
||||
>
|
||||
<DuplicateIcon />
|
||||
</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 {extensionObject.lastEditor.displayName}
|
||||
<br />
|
||||
at{" "}
|
||||
{format(
|
||||
extensionObject.lastEditor.lastUpdate,
|
||||
DATE_TIME_FORMAT
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.disabled" }}
|
||||
>
|
||||
{formatRelative(
|
||||
extensionObject.lastEditor.lastUpdate,
|
||||
new Date()
|
||||
)}
|
||||
</Typography>
|
||||
<Avatar
|
||||
alt={`${extensionObject.lastEditor.displayName}’s profile photo`}
|
||||
src={extensionObject.lastEditor.photoURL}
|
||||
sx={{ width: 24, height: 24, "&&": { mr: -0.5 } }}
|
||||
/>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
<List style={{ minHeight: 89 * 3 - 1 }} disablePadding>
|
||||
{extensions.map((extensionObject, index) => (
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense={false}
|
||||
divider={index !== extensions.length - 1}
|
||||
children={
|
||||
<ListItemText
|
||||
primary={extensionObject.name}
|
||||
secondary={extensionNames[extensionObject.type]}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
minHeight: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
secondaryAction={
|
||||
<Stack alignItems="flex-end">
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip
|
||||
title={extensionObject.active ? "Deactivate" : "Activate"}
|
||||
>
|
||||
<Switch
|
||||
checked={extensionObject.active}
|
||||
onClick={() =>
|
||||
handleUpdateActive(index, !extensionObject.active)
|
||||
}
|
||||
inputProps={{ "aria-label": "Activate" }}
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Duplicate">
|
||||
<IconButton
|
||||
aria-label="Duplicate"
|
||||
onClick={() => handleDuplicate(index)}
|
||||
>
|
||||
<DuplicateIcon />
|
||||
</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 {extensionObject.lastEditor.displayName}
|
||||
<br />
|
||||
at{" "}
|
||||
{format(
|
||||
extensionObject.lastEditor.lastUpdate,
|
||||
DATE_TIME_FORMAT
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography variant="body2" sx={{ color: "text.disabled" }}>
|
||||
{formatRelative(
|
||||
extensionObject.lastEditor.lastUpdate,
|
||||
new Date()
|
||||
)}
|
||||
</Typography>
|
||||
<Avatar
|
||||
alt={`${extensionObject.lastEditor.displayName}’s profile photo`}
|
||||
src={extensionObject.lastEditor.photoURL}
|
||||
sx={{ width: 24, height: 24, "&&": { mr: -0.5 } }}
|
||||
/>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
sx={{
|
||||
flexWrap: { xs: "wrap", sm: "nowrap" },
|
||||
"& .MuiListItemSecondaryAction-root": {
|
||||
position: { xs: "static", sm: "absolute" },
|
||||
width: { xs: "100%", sm: "auto" },
|
||||
transform: { xs: "none", sm: "translateY(-50%)" },
|
||||
},
|
||||
pr: { xs: 0, sm: 216 / 8 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
|
||||
import { Breadcrumbs } from "@mui/material";
|
||||
|
||||
import TableHeaderButton from "../TableHeaderButton";
|
||||
import ExtensionIcon from "@src/assets/icons/Extension";
|
||||
import Modal from "@src/components/Modal";
|
||||
import AddExtensionButton from "./AddExtensionButton";
|
||||
import ExtensionList from "./ExtensionList";
|
||||
import ExtensionModal from "./ExtensionModal";
|
||||
import ExtensionMigration from "./ExtensionMigration";
|
||||
@@ -40,12 +39,6 @@ export default function Extensions() {
|
||||
const snackLogContext = useSnackLogContext();
|
||||
const edited = !_isEqual(currentExtensionObjects, localExtensionsObjects);
|
||||
|
||||
const tablePathTokens =
|
||||
tableState?.tablePath?.split("/").filter(function (_, i) {
|
||||
// replace IDs with dash that appears at even indexes
|
||||
return i % 2 === 0;
|
||||
}) ?? [];
|
||||
|
||||
const handleOpen = () => {
|
||||
if (tableState?.config.sparks) {
|
||||
// migration is required
|
||||
@@ -180,6 +173,10 @@ export default function Extensions() {
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
|
||||
const activeExtensionCount = localExtensionsObjects.filter(
|
||||
(extension) => extension.active
|
||||
).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderButton
|
||||
@@ -191,33 +188,32 @@ export default function Extensions() {
|
||||
{openExtensionList && !!tableState && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
disableBackdropClick={edited}
|
||||
disableEscapeKeyDown={edited}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title="Extensions"
|
||||
title={`Extensions (${activeExtensionCount}\u2009/\u2009${localExtensionsObjects.length})`}
|
||||
header={
|
||||
<AddExtensionButton
|
||||
handleAddExtension={(type: ExtensionType) => {
|
||||
setExtensionModal({
|
||||
mode: "add",
|
||||
extensionObject: emptyExtensionObject(type, currentEditor()),
|
||||
});
|
||||
}}
|
||||
variant={
|
||||
localExtensionsObjects.length === 0 ? "contained" : "outlined"
|
||||
}
|
||||
/>
|
||||
}
|
||||
children={
|
||||
<>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
{tablePathTokens.map((pathToken) => (
|
||||
<code>{pathToken}</code>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<ExtensionList
|
||||
extensions={localExtensionsObjects}
|
||||
handleAddExtension={(type: ExtensionType) => {
|
||||
setExtensionModal({
|
||||
mode: "add",
|
||||
extensionObject: emptyExtensionObject(
|
||||
type,
|
||||
currentEditor()
|
||||
),
|
||||
});
|
||||
}}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleDuplicate={handleDuplicate}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
<ExtensionList
|
||||
extensions={localExtensionsObjects}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleDuplicate={handleDuplicate}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
|
||||
83
src/components/TableHeader/Webhooks/AddWebhookButton.tsx
Normal file
83
src/components/TableHeader/Webhooks/AddWebhookButton.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import EmailIcon from "@mui/icons-material/EmailOutlined";
|
||||
|
||||
import { webhookTypes, webhookNames, WebhookType } from "./utils";
|
||||
import { EMAIL_REQUEST } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IAddWebhookButtonProps extends Partial<ButtonProps> {
|
||||
handleAddWebhook: (type: WebhookType) => void;
|
||||
}
|
||||
|
||||
export default function AddWebhookButton({
|
||||
handleAddWebhook,
|
||||
...props
|
||||
}: IAddWebhookButtonProps) {
|
||||
const addButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleChooseAddType = (type: WebhookType) => {
|
||||
setOpen(false);
|
||||
handleAddWebhook(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
ref={addButtonRef}
|
||||
sx={{
|
||||
alignSelf: { sm: "flex-end" },
|
||||
mt: {
|
||||
sm: `calc(var(--dialog-title-height) * -1 + (var(--dialog-title-height) - 32px) / 2)`,
|
||||
},
|
||||
mx: { xs: "var(--dialog-spacing)", sm: undefined },
|
||||
mr: { sm: 8 },
|
||||
mb: { xs: 1.5, sm: 2 },
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Add webhook…
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
anchorEl={addButtonRef.current}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
{webhookTypes.map((type) => (
|
||||
<MenuItem onClick={() => handleChooseAddType(type)}>
|
||||
{webhookNames[type]}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EMAIL_REQUEST}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EmailIcon aria-label="Send email" />
|
||||
</ListItemIcon>
|
||||
Request new webhook…
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +1,27 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
|
||||
import {
|
||||
Stack,
|
||||
ButtonBase,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
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 CopyIcon from "@src/assets/icons/Copy";
|
||||
import EmailIcon from "@mui/icons-material/EmailOutlined";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import { webhookTypes, webhookNames, IWebhook, WebhookType } from "./utils";
|
||||
import { webhookNames, IWebhook } from "./utils";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { EMAIL_REQUEST } from "@src/constants/externalLinks";
|
||||
import {
|
||||
modalAtom,
|
||||
cloudLogFiltersAtom,
|
||||
@@ -39,7 +29,6 @@ import {
|
||||
|
||||
export interface IWebhookListProps {
|
||||
webhooks: IWebhook[];
|
||||
handleAddWebhook: (type: WebhookType) => void;
|
||||
handleUpdateActive: (index: number, active: boolean) => void;
|
||||
handleEdit: (index: number) => void;
|
||||
handleDelete: (index: number) => void;
|
||||
@@ -47,243 +36,157 @@ export interface IWebhookListProps {
|
||||
|
||||
export default function WebhookList({
|
||||
webhooks,
|
||||
handleAddWebhook,
|
||||
handleUpdateActive,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
}: IWebhookListProps) {
|
||||
const { settings, tableState } = useProjectContext();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const addButtonRef = useRef(null);
|
||||
|
||||
const [, setModal] = useAtom(modalAtom);
|
||||
const [, setCloudLogFilters] = useAtom(cloudLogFiltersAtom);
|
||||
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?.services?.hooks}/wh/${tableState?.tablePath}/`;
|
||||
|
||||
if (webhooks.length === 0)
|
||||
return (
|
||||
<EmptyState
|
||||
message="Add your first webhook above"
|
||||
description="Your webhooks will appear here"
|
||||
Icon={WebhookIcon}
|
||||
style={{ height: 89 * 3 - 1 }}
|
||||
/>
|
||||
);
|
||||
|
||||
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>
|
||||
))}
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EMAIL_REQUEST}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EmailIcon aria-label="Send email" />
|
||||
</ListItemIcon>
|
||||
Request new webhook…
|
||||
</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>
|
||||
}
|
||||
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={() => {
|
||||
setModal("cloudLogs");
|
||||
setCloudLogFilters({
|
||||
type: "webhook",
|
||||
timeRange: { type: "days", value: 7 },
|
||||
webhook: [webhook.endpoint],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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
|
||||
)}
|
||||
</>
|
||||
}
|
||||
<List style={{ paddingTop: 0, minHeight: 89 * 3 - 1 }} disablePadding>
|
||||
{webhooks.map((webhook, index) => (
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense={false}
|
||||
divider={index !== webhooks.length - 1}
|
||||
children={
|
||||
<ListItemText
|
||||
primary={webhook.name}
|
||||
secondary={
|
||||
<>
|
||||
{webhookNames[webhook.type]}{" "}
|
||||
<code
|
||||
style={{
|
||||
userSelect: "all",
|
||||
paddingRight: 0,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "text.disabled" }}
|
||||
<Tooltip title="Endpoint ID">
|
||||
<span>{webhook.endpoint}</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy endpoint URL">
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(
|
||||
baseUrl + webhook.endpoint
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
color="secondary"
|
||||
sx={{ my: (20 - 32) / 2 / 8 }}
|
||||
>
|
||||
{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>
|
||||
<LinkIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</code>
|
||||
</>
|
||||
}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
minHeight: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
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={() => {
|
||||
setModal("cloudLogs");
|
||||
setCloudLogFilters({
|
||||
type: "webhook",
|
||||
timeRange: { type: "days", value: 7 },
|
||||
webhook: [webhook.endpoint],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
sx={{
|
||||
flexWrap: { xs: "wrap", sm: "nowrap" },
|
||||
"& .MuiListItemSecondaryAction-root": {
|
||||
position: { xs: "static", sm: "absolute" },
|
||||
width: { xs: "100%", sm: "auto" },
|
||||
transform: { xs: "none", sm: "translateY(-50%)" },
|
||||
},
|
||||
pr: { xs: 0, sm: 216 / 8 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 AddWebhookButton from "./AddWebhookButton";
|
||||
import WebhookList from "./WebhookList";
|
||||
import WebhookModal from "./WebhookModal";
|
||||
|
||||
@@ -34,14 +33,10 @@ export default function Webhooks() {
|
||||
webhookObject: IWebhook;
|
||||
index?: number;
|
||||
} | null>(null);
|
||||
if (!compatibleRowyRunVersion?.({ minVersion: "1.2.0" })) 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;
|
||||
}) ?? [];
|
||||
if (!compatibleRowyRunVersion?.({ minVersion: "1.2.0" })) return null;
|
||||
|
||||
const edited = !_isEqual(currentWebhooks, localWebhooksObjects);
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpenWebhookList(true);
|
||||
@@ -159,6 +154,10 @@ export default function Webhooks() {
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
|
||||
const activeWebhookCount = localWebhooksObjects.filter(
|
||||
(webhook) => webhook.active
|
||||
).length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderButton
|
||||
@@ -170,29 +169,31 @@ export default function Webhooks() {
|
||||
{openWebhookList && !!tableState && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
disableBackdropClick={edited}
|
||||
disableEscapeKeyDown={edited}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title="Webhooks"
|
||||
title={`Webhooks (${activeWebhookCount}\u2009/\u2009${localWebhooksObjects.length})`}
|
||||
header={
|
||||
<AddWebhookButton
|
||||
handleAddWebhook={(type: WebhookType) => {
|
||||
setWebhookModal({
|
||||
mode: "add",
|
||||
webhookObject: emptyWebhookObject(type, currentEditor()),
|
||||
});
|
||||
}}
|
||||
variant={
|
||||
localWebhooksObjects.length === 0 ? "contained" : "outlined"
|
||||
}
|
||||
/>
|
||||
}
|
||||
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}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
</>
|
||||
<WebhookList
|
||||
webhooks={localWebhooksObjects}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DataGridHandle } from "react-data-grid";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _find from "lodash/find";
|
||||
import firebase from "firebase/app";
|
||||
import { compare } from "compare-versions";
|
||||
|
||||
import { Button } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
@@ -19,7 +20,6 @@ import { rowyRun, IRowyRunRequestProps } from "@src/utils/rowyRun";
|
||||
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;
|
||||
@@ -375,13 +375,12 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
|
||||
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;
|
||||
if (!rowyRunVersion) return false;
|
||||
if (minVersion && compare(rowyRunVersion, minVersion, "<")) return false;
|
||||
if (maxVersion && compare(rowyRunVersion, maxVersion, ">")) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// A ref to the data grid. Contains data grid functions
|
||||
const dataGridRef = useRef<DataGridHandle>(null);
|
||||
const sideDrawerRef = useRef<SideDrawerRef>();
|
||||
|
||||
Reference in New Issue
Block a user