From 12d196b3c56c406d2258847afeeca14119a6711d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nuno=20G=C3=B3is?=
Date: Tue, 20 Feb 2024 13:04:16 +0000
Subject: [PATCH 1/6] chore: incoming webhook events UI
---
.../common/FormTemplate/FormTemplate.tsx | 6 +-
.../common/SidePanelList/SidePanelList.tsx | 3 +
.../IncomingWebhooksEventsModal.tsx | 129 ++++++++++++++++++
.../IncomingWebhooksModal.tsx | 22 ++-
.../IncomingWebhooksActionsCell.tsx | 22 ++-
.../IncomingWebhooksTable.tsx | 20 +++
.../useIncomingWebhookEvents.ts | 49 +++++++
frontend/src/interfaces/incomingWebhook.ts | 11 ++
8 files changed, 257 insertions(+), 5 deletions(-)
create mode 100644 frontend/src/component/common/SidePanelList/SidePanelList.tsx
create mode 100644 frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
create mode 100644 frontend/src/hooks/api/getters/useIncomingWebhookEvents/useIncomingWebhookEvents.ts
diff --git a/frontend/src/component/common/FormTemplate/FormTemplate.tsx b/frontend/src/component/common/FormTemplate/FormTemplate.tsx
index a482aa786a89..6ad462553c9c 100644
--- a/frontend/src/component/common/FormTemplate/FormTemplate.tsx
+++ b/frontend/src/component/common/FormTemplate/FormTemplate.tsx
@@ -32,6 +32,7 @@ interface ICreateProps {
formatApiCode?: () => string;
footer?: ReactNode;
compact?: boolean;
+ showGuidance?: boolean;
}
const StyledContainer = styled('section', {
@@ -202,6 +203,7 @@ const FormTemplate: React.FC = ({
showLink = true,
footer,
compact,
+ showGuidance = true,
}) => {
const { setToastData } = useToast();
const smallScreen = useMediaQuery(`(max-width:${1099}px)`);
@@ -252,7 +254,7 @@ const FormTemplate: React.FC = ({
return (
= ({
/>
{
+ return TODO: Implement;
+};
diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
new file mode 100644
index 000000000000..50c6272df724
--- /dev/null
+++ b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
@@ -0,0 +1,129 @@
+import { Button, Link, styled } from '@mui/material';
+import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
+import { IIncomingWebhook } from 'interfaces/incomingWebhook';
+import { useIncomingWebhookEvents } from 'hooks/api/getters/useIncomingWebhookEvents/useIncomingWebhookEvents';
+import { useState } from 'react';
+import FormTemplate from 'component/common/FormTemplate/FormTemplate';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import { SidePanelList } from 'component/common/SidePanelList/SidePanelList';
+
+const StyledHeader = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ marginBottom: theme.fontSizes.mainHeader,
+}));
+
+const StyledHeaderRow = styled('div')({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+});
+
+const StyledHeaderSubtitle = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ marginTop: theme.spacing(2),
+ fontSize: theme.fontSizes.smallBody,
+}));
+
+const StyledDescription = styled('p')(({ theme }) => ({
+ color: theme.palette.text.secondary,
+}));
+
+const StyledTitle = styled('h1')({
+ fontWeight: 'normal',
+});
+
+const StyledForm = styled('form')(() => ({
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+}));
+
+const StyledButtonContainer = styled('div')(({ theme }) => ({
+ marginTop: 'auto',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ paddingTop: theme.spacing(4),
+}));
+
+const LIMIT = 50;
+
+interface IIncomingWebhooksEventsModalProps {
+ incomingWebhook?: IIncomingWebhook;
+ open: boolean;
+ setOpen: React.Dispatch>;
+ onOpenConfiguration: () => void;
+}
+
+export const IncomingWebhooksEventsModal = ({
+ incomingWebhook,
+ open,
+ setOpen,
+ onOpenConfiguration,
+}: IIncomingWebhooksEventsModalProps) => {
+ const { uiConfig } = useUiConfig();
+ const [page, setPage] = useState(0);
+ const { incomingWebhookEvents, loading } = useIncomingWebhookEvents(
+ incomingWebhook?.id,
+ LIMIT,
+ page * LIMIT,
+ );
+
+ if (!incomingWebhook) {
+ return null;
+ }
+
+ const title = `Events: ${incomingWebhook.name}`;
+
+ return (
+ {
+ setOpen(false);
+ }}
+ label={title}
+ >
+
+
+
+ {title}
+
+ View configuration
+
+
+
+
+ {uiConfig.unleashUrl}/api/incoming-webhook/
+ {incomingWebhook.name}
+
+
+ {incomingWebhook.description}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
index 4301b7300b73..ff789d63c7d1 100644
--- a/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
+++ b/frontend/src/component/incomingWebhooks/IncomingWebhooksModal/IncomingWebhooksModal.tsx
@@ -1,5 +1,5 @@
import { FormEvent, useEffect } from 'react';
-import { Button, styled } from '@mui/material';
+import { Button, Link, styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
@@ -18,6 +18,18 @@ import {
useIncomingWebhooksForm,
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
+const StyledHeader = styled('div')(({ theme }) => ({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ width: '100%',
+ marginBottom: theme.fontSizes.mainHeader,
+}));
+
+const StyledTitle = styled('h1')({
+ fontWeight: 'normal',
+});
+
const StyledForm = styled('form')(() => ({
display: 'flex',
flexDirection: 'column',
@@ -40,6 +52,7 @@ interface IIncomingWebhooksModalProps {
open: boolean;
setOpen: React.Dispatch>;
newToken: (token: string) => void;
+ onOpenEvents: () => void;
}
export const IncomingWebhooksModal = ({
@@ -47,6 +60,7 @@ export const IncomingWebhooksModal = ({
open,
setOpen,
newToken,
+ onOpenEvents,
}: IIncomingWebhooksModalProps) => {
const { refetch } = useIncomingWebhooks();
const { addIncomingWebhook, updateIncomingWebhook, loading } =
@@ -137,12 +151,16 @@ export const IncomingWebhooksModal = ({
+
+ {title}
+ View events
+
void;
+ onOpenEvents: (event: React.SyntheticEvent) => void;
onEdit: (event: React.SyntheticEvent) => void;
onDelete: (event: React.SyntheticEvent) => void;
}
@@ -33,6 +34,7 @@ interface IIncomingWebhooksActionsCellProps {
export const IncomingWebhooksActionsCell = ({
incomingWebhookId,
onCopyToClipboard,
+ onOpenEvents,
onEdit,
onDelete,
}: IIncomingWebhooksActionsCellProps) => {
@@ -94,6 +96,24 @@ export const IncomingWebhooksActionsCell = ({
Copy URL
+
+ {({ hasAccess }) => (
+
+ )}
+
{({ hasAccess }) => (
+ }
+ />
+ }
+ />
+ }
/>
{
+ const response = await fetch(url);
+ await handleErrorResponses('Incoming webhook events')(response);
+ return response.json();
};
export const useIncomingWebhookEvents = (
incomingWebhookId?: number,
limit = 50,
- offset = 0,
+ options: SWRInfiniteConfiguration = {},
) => {
const { isEnterprise } = useUiConfig();
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
- const { data, error, mutate } = useConditionalSWR<{
- incomingWebhookEvents: IIncomingWebhookEvent[];
- }>(
- Boolean(incomingWebhookId) && isEnterprise() && incomingWebhooksEnabled,
- DEFAULT_DATA,
- formatApiPath(
- `${ENDPOINT}/${incomingWebhookId}/events?limit=${limit}&offset=${offset}`,
- ),
- fetcher,
- );
-
- return useMemo(
- () => ({
- incomingWebhookEvents: data?.incomingWebhookEvents ?? [],
- loading: !error && !data,
- refetch: () => mutate(),
- error,
- }),
- [data, error, mutate],
- );
-};
+ const getKey: SWRInfiniteKeyLoader = (
+ pageIndex: number,
+ previousPageData: IncomingWebhookEventsResponse,
+ ) => {
+ // Does not meet conditions
+ if (!incomingWebhookId || !isEnterprise || !incomingWebhooksEnabled)
+ return null;
+
+ // Reached the end
+ if (previousPageData && !previousPageData.incomingWebhookEvents.length)
+ return null;
+
+ return formatApiPath(
+ `${ENDPOINT}/${incomingWebhookId}/events?limit=${limit}&offset=${
+ pageIndex * limit
+ }`,
+ );
+ };
+
+ const { data, error, size, setSize, mutate } =
+ useSWRInfinite(getKey, fetcher, {
+ ...options,
+ revalidateAll: true,
+ });
+
+ const incomingWebhookEvents = data
+ ? data.flatMap(({ incomingWebhookEvents }) => incomingWebhookEvents)
+ : [];
+
+ const isLoadingInitialData = !data && !error;
+ const isLoadingMore = size > 0 && !data?.[size - 1];
+ const loading = isLoadingInitialData || isLoadingMore;
+
+ const hasMore = data?.[size - 1]?.incomingWebhookEvents.length === limit;
+
+ const loadMore = () => {
+ if (loading || !hasMore) return;
+ setSize(size + 1);
+ };
-const fetcher = (path: string) => {
- return fetch(path)
- .then(handleErrorResponses('Incoming webhook events'))
- .then((res) => res.json());
+ return {
+ incomingWebhookEvents,
+ hasMore,
+ loadMore,
+ loading,
+ refetch: () => mutate(),
+ error,
+ };
};
From f35add8c1545efc11cba516126b63b8cd956f581 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nuno=20G=C3=B3is?=
Date: Fri, 23 Feb 2024 10:21:37 +0000
Subject: [PATCH 6/6] fix: remove empty title prop
---
.../IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx | 1 -
.../IncomingWebhooksModal/IncomingWebhooksModal.tsx | 1 -
2 files changed, 2 deletions(-)
diff --git a/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
index 13b9906c0c65..3bb7ac0a534b 100644
--- a/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
+++ b/frontend/src/component/incomingWebhooks/IncomingWebhooksEvents/IncomingWebhooksEventsModal.tsx
@@ -92,7 +92,6 @@ export const IncomingWebhooksEventsModal = ({