fix webhooks, extensions styling

This commit is contained in:
Sidney Alcantara
2021-11-29 14:37:36 +11:00
parent e339d0e7dd
commit 6b063dba3d
8 changed files with 483 additions and 512 deletions

View File

@@ -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",

View 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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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: {

View 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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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: {

View File

@@ -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>();