1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00
Nuno Góis 2024-02-27 13:52:09 +00:00 committed by GitHub
parent 9101c39eb7
commit 477a9c6cfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 680 additions and 35 deletions

View File

@ -21,13 +21,14 @@ const StyledSidePanelHalf = styled('div')({
});
const StyledSidePanelHalfLeft = styled(StyledSidePanelHalf, {
shouldForwardProp: (prop) => prop !== 'height',
})<{ height?: number }>(({ theme, height }) => ({
shouldForwardProp: (prop) => prop !== 'height' && prop !== 'maxWidth',
})<{ height?: number; maxWidth?: number }>(({ theme, height, maxWidth }) => ({
border: `1px solid ${theme.palette.divider}`,
borderTop: 0,
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
overflow: 'auto',
...(height && { height }),
...(maxWidth && { maxWidth }),
}));
const StyledSidePanelHalfRight = styled(StyledSidePanelHalf)(({ theme }) => ({
@ -43,12 +44,14 @@ export const StyledSidePanelListColumn = styled('div', {
shouldForwardProp: (prop) => prop !== 'maxWidth' && prop !== 'align',
})<{ maxWidth?: number; align?: ColumnAlignment }>(
({ theme, maxWidth, align = 'start' }) => ({
display: 'flex',
flex: 1,
padding: theme.spacing(2),
fontSize: theme.fontSizes.smallBody,
justifyContent: align,
...(maxWidth && { maxWidth }),
textAlign: align,
alignItems: 'center',
}),
);
@ -64,6 +67,7 @@ interface ISidePanelListProps<T> {
columns: SidePanelListColumn<T>[];
sidePanelHeader: string;
renderContent: (item: T) => ReactNode;
renderItem?: (item: T, children: ReactNode) => ReactNode;
height?: number;
listEnd?: ReactNode;
}
@ -73,6 +77,7 @@ export const SidePanelList = <T extends { id: string | number }>({
columns,
sidePanelHeader,
renderContent,
renderItem = (_, children) => children,
height,
listEnd,
}: ISidePanelListProps<T>) => {
@ -83,16 +88,25 @@ export const SidePanelList = <T extends { id: string | number }>({
}
const activeItem = selectedItem || items[0];
const leftPanelMaxWidth = columns.every(({ maxWidth }) => Boolean(maxWidth))
? columns.reduce((acc, { maxWidth }) => acc + (maxWidth || 0), 0)
: undefined;
return (
<StyledSidePanelListWrapper>
<SidePanelListHeader
columns={columns}
sidePanelHeader={sidePanelHeader}
leftPanelMaxWidth={leftPanelMaxWidth}
/>
<StyledSidePanelListBody>
<StyledSidePanelHalfLeft height={height}>
{items.map((item) => (
<StyledSidePanelHalfLeft
height={height}
maxWidth={leftPanelMaxWidth}
>
{items.map((item) =>
renderItem(
item,
<SidePanelListItem
key={item.id}
selected={activeItem.id === item.id}
@ -109,8 +123,9 @@ export const SidePanelList = <T extends { id: string | number }>({
</StyledSidePanelListColumn>
),
)}
</SidePanelListItem>
))}
</SidePanelListItem>,
),
)}
{listEnd}
</StyledSidePanelHalfLeft>
<StyledSidePanelHalfRight>

View File

@ -13,22 +13,27 @@ const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.table.headerBackground,
}));
const StyledHeaderHalf = styled('div')({
const StyledHeaderHalf = styled('div', {
shouldForwardProp: (prop) => prop !== 'maxWidth',
})<{ maxWidth?: number }>(({ maxWidth }) => ({
display: 'flex',
flex: 1,
});
...(maxWidth && { maxWidth }),
}));
interface ISidePanelListHeaderProps<T> {
columns: SidePanelListColumn<T>[];
sidePanelHeader: string;
leftPanelMaxWidth?: number;
}
export const SidePanelListHeader = <T,>({
columns,
sidePanelHeader,
leftPanelMaxWidth,
}: ISidePanelListHeaderProps<T>) => (
<StyledHeader>
<StyledHeaderHalf>
<StyledHeaderHalf maxWidth={leftPanelMaxWidth}>
{columns.map(({ header, maxWidth, align }) => (
<StyledSidePanelListColumn
key={header}

View File

@ -39,17 +39,17 @@ const StyledItem = styled(Button, {
},
}));
interface ISidePanelListItemProps<T> {
interface ISidePanelListItemProps {
selected: boolean;
onClick: () => void;
children: ReactNode;
}
export const SidePanelListItem = <T,>({
export const SidePanelListItem = ({
selected,
onClick,
children,
}: ISidePanelListItemProps<T>) => (
}: ISidePanelListItemProps) => (
<StyledItemRow>
<StyledItem selected={selected} onClick={onClick}>
{children}

View File

@ -121,6 +121,7 @@ export const IncomingWebhooksEventsModal = ({
columns={[
{
header: 'Date',
maxWidth: 180,
cell: (event) =>
formatDateYMDHMS(
event.createdAt,
@ -129,6 +130,7 @@ export const IncomingWebhooksEventsModal = ({
},
{
header: 'Token',
maxWidth: 350,
cell: (event) => event.tokenName,
},
]}

View File

@ -17,6 +17,7 @@ import {
TokenGeneration,
useIncomingWebhooksForm,
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
@ -158,7 +159,10 @@ export const IncomingWebhooksModal = ({
>
<StyledHeader>
<StyledTitle>{title}</StyledTitle>
<Link onClick={onOpenEvents}>View events</Link>
<ConditionallyRender
condition={editing}
show={<Link onClick={onOpenEvents}>View events</Link>}
/>
</StyledHeader>
<StyledForm onSubmit={onSubmit}>
<IncomingWebhooksForm

View File

@ -0,0 +1,43 @@
import { Alert, styled } from '@mui/material';
import { IActionSetEvent } from 'interfaces/action';
import { ProjectActionsEventsDetailsAction } from './ProjectActionsEventsDetailsAction';
import { ProjectActionsEventsDetailsSource } from './ProjectActionsEventsDetailsSource/ProjectActionsEventsDetailsSource';
const StyledDetails = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(2),
}));
export const ProjectActionsEventsDetails = ({
state,
actionSet: { actions },
observableEvent,
}: IActionSetEvent) => {
const stateText =
state === 'failed'
? `${
actions.filter(({ state }) => state !== 'success').length
} out of ${actions.length} actions were not successfully executed`
: 'All actions were successfully executed';
return (
<StyledDetails>
<Alert severity={state === 'failed' ? 'error' : 'success'}>
{stateText}
</Alert>
<ProjectActionsEventsDetailsSource
observableEvent={observableEvent}
/>
{actions.map((action, i) => (
<ProjectActionsEventsDetailsAction
key={action.id}
action={action}
>
Action {i + 1}
</ProjectActionsEventsDetailsAction>
))}
</StyledDetails>
);
};

View File

@ -0,0 +1,110 @@
import { CheckCircleOutline, ErrorOutline } from '@mui/icons-material';
import { Alert, CircularProgress, Divider, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IActionEvent } from 'interfaces/action';
import { ReactNode } from 'react';
const StyledAction = styled('div', {
shouldForwardProp: (prop) => prop !== 'state',
})<{ state?: IActionEvent['state'] }>(({ theme, state }) => ({
padding: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
...(state === 'not started' && {
backgroundColor: theme.palette.background.elevation1,
}),
}));
const StyledHeader = styled('div')({
display: 'flex',
flexDirection: 'column',
});
const StyledHeaderRow = styled('div')({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
});
const StyledHeaderState = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
fontSize: theme.fontSizes.smallBody,
gap: theme.spacing(2),
}));
export const StyledSuccessIcon = styled(CheckCircleOutline)(({ theme }) => ({
color: theme.palette.success.main,
}));
export const StyledFailedIcon = styled(ErrorOutline)(({ theme }) => ({
color: theme.palette.error.main,
}));
const StyledAlert = styled(Alert)(({ theme }) => ({
marginTop: theme.spacing(2),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(2, 0),
}));
const StyledActionBody = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledActionLabel = styled('p')(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
marginBottom: theme.spacing(0.5),
}));
const StyledPropertyLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
interface IProjectActionsEventsDetailsActionProps {
action: IActionEvent;
children: ReactNode;
}
export const ProjectActionsEventsDetailsAction = ({
action: { state, details, action, executionParams },
children,
}: IProjectActionsEventsDetailsActionProps) => {
const actionState =
state === 'success' ? (
<StyledSuccessIcon />
) : state === 'failed' ? (
<StyledFailedIcon />
) : state === 'started' ? (
<CircularProgress size={20} />
) : (
<span>Not started</span>
);
return (
<StyledAction state={state}>
<StyledHeader>
<StyledHeaderRow>
<div>{children}</div>
<StyledHeaderState>{actionState}</StyledHeaderState>
</StyledHeaderRow>
<ConditionallyRender
condition={Boolean(details)}
show={<StyledAlert severity='error'>{details}</StyledAlert>}
/>
</StyledHeader>
<StyledDivider />
<StyledActionBody>
<StyledActionLabel>{action}</StyledActionLabel>
{Object.entries(executionParams).map(([property, value]) => (
<div key={property}>
<StyledPropertyLabel>{property}:</StyledPropertyLabel>{' '}
{value}
</div>
))}
</StyledActionBody>
</StyledAction>
);
};

View File

@ -0,0 +1,22 @@
import { IObservableEvent } from 'interfaces/action';
import { ProjectActionsEventsDetailsSourceIncomingWebhook } from './ProjectActionsEventsDetailsSourceIncomingWebhook';
interface IProjectActionsEventsDetailsSourceProps {
observableEvent: IObservableEvent;
}
export const ProjectActionsEventsDetailsSource = ({
observableEvent,
}: IProjectActionsEventsDetailsSourceProps) => {
const { source } = observableEvent;
if (source === 'incoming-webhook') {
return (
<ProjectActionsEventsDetailsSourceIncomingWebhook
observableEvent={observableEvent}
/>
);
}
return null;
};

View File

@ -0,0 +1,75 @@
import { ExpandMore } from '@mui/icons-material';
import {
Accordion,
AccordionDetails,
AccordionSummary,
IconButton,
styled,
} from '@mui/material';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
import { IObservableEvent } from 'interfaces/action';
import { Suspense, lazy, useMemo } from 'react';
import { Link } from 'react-router-dom';
const LazyReactJSONEditor = lazy(
() => import('component/common/ReactJSONEditor/ReactJSONEditor'),
);
const StyledAccordion = styled(Accordion)(({ theme }) => ({
boxShadow: 'none',
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
'&:before': {
display: 'none',
},
}));
const StyledLink = styled(Link)(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
interface IProjectActionsEventsDetailsSourceIncomingWebhookProps {
observableEvent: IObservableEvent;
}
export const ProjectActionsEventsDetailsSourceIncomingWebhook = ({
observableEvent,
}: IProjectActionsEventsDetailsSourceIncomingWebhookProps) => {
const { incomingWebhooks } = useIncomingWebhooks();
const incomingWebhookName = useMemo(() => {
const incomingWebhook = incomingWebhooks.find(
(incomingWebhook) =>
incomingWebhook.id === observableEvent.sourceId,
);
return incomingWebhook?.name;
}, [incomingWebhooks, observableEvent.sourceId]);
return (
<StyledAccordion>
<AccordionSummary
expandIcon={
<IconButton>
<ExpandMore titleAccess='Toggle' />
</IconButton>
}
>
Incoming webhook:
<StyledLink to='/integrations/incoming-webhooks'>
{incomingWebhookName}
</StyledLink>
</AccordionSummary>
<AccordionDetails>
<Suspense fallback={null}>
<LazyReactJSONEditor
content={{ json: observableEvent.payload }}
readOnly
statusBar={false}
editorStyle='sidePanel'
/>
</Suspense>
</AccordionDetails>
</StyledAccordion>
);
};

View File

@ -0,0 +1,169 @@
import { Button, Link, styled } from '@mui/material';
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
import { IActionSet } from 'interfaces/action';
import { useActionEvents } from 'hooks/api/getters/useActionEvents/useActionEvents';
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { SidePanelList } from 'component/common/SidePanelList/SidePanelList';
import { formatDateYMDHMS } from 'utils/formatDate';
import { useLocationSettings } from 'hooks/useLocationSettings';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ProjectActionsEventsStateCell } from './ProjectActionsEventsStateCell';
import { ProjectActionsEventsDetails } from './ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails';
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 StyledTitle = styled('h1')({
fontWeight: 'normal',
});
const StyledForm = styled('form')({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
const StyledFailedItemWrapper = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.error.light,
}));
const StyledButtonContainer = styled('div')(({ theme }) => ({
marginTop: 'auto',
display: 'flex',
justifyContent: 'flex-end',
paddingTop: theme.spacing(4),
}));
interface IProjectActionsEventsModalProps {
action?: IActionSet;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onOpenConfiguration: () => void;
}
export const ProjectActionsEventsModal = ({
action,
open,
setOpen,
onOpenConfiguration,
}: IProjectActionsEventsModalProps) => {
const projectId = useRequiredPathParam('projectId');
const { locationSettings } = useLocationSettings();
const { actionEvents, hasMore, loadMore, loading } = useActionEvents(
action?.id,
projectId,
20,
{
refreshInterval: 5000,
},
);
if (!action) {
return null;
}
const title = `Events: ${action.name}`;
return (
<SidebarModal
open={open}
onClose={() => {
setOpen(false);
}}
label={title}
>
<FormTemplate
loading={loading && actionEvents.length === 0}
modal
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.'
documentationLink='https://docs.getunleash.io/reference/actions'
documentationLinkLabel='Actions documentation'
showGuidance={false}
>
<StyledHeader>
<StyledHeaderRow>
<StyledTitle>{title}</StyledTitle>
<Link onClick={onOpenConfiguration}>
View configuration
</Link>
</StyledHeaderRow>
</StyledHeader>
<StyledForm>
<SidePanelList
height={960}
items={actionEvents}
columns={[
{
header: 'Status',
align: 'center',
maxWidth: 100,
cell: ProjectActionsEventsStateCell,
},
{
header: 'Date',
maxWidth: 240,
cell: ({ createdAt }) =>
formatDateYMDHMS(
createdAt,
locationSettings?.locale,
),
},
]}
sidePanelHeader='Details'
renderContent={ProjectActionsEventsDetails}
renderItem={({ id, state }, children) => {
if (state === 'failed') {
return (
<StyledFailedItemWrapper key={id}>
{children}
</StyledFailedItemWrapper>
);
}
return children;
}}
listEnd={
<ConditionallyRender
condition={hasMore}
show={
<Button onClick={loadMore}>
Load more
</Button>
}
/>
}
/>
<ConditionallyRender
condition={actionEvents.length === 0}
show={
<p>
No events have been registered for this action
set.
</p>
}
/>
<StyledButtonContainer>
<Button
onClick={() => {
setOpen(false);
}}
>
Close
</Button>
</StyledButtonContainer>
</StyledForm>
</FormTemplate>
</SidebarModal>
);
};

View File

@ -0,0 +1,23 @@
import { CircularProgress, styled } from '@mui/material';
import { CheckCircle, Error as ErrorIcon } from '@mui/icons-material';
import { IActionSetEvent } from 'interfaces/action';
export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
color: theme.palette.success.main,
}));
export const StyledFailedIcon = styled(ErrorIcon)(({ theme }) => ({
color: theme.palette.error.main,
}));
export const ProjectActionsEventsStateCell = ({ state }: IActionSetEvent) => {
if (state === 'success') {
return <StyledSuccessIcon />;
}
if (state === 'failed') {
return <StyledFailedIcon />;
}
return <CircularProgress size={20} />;
};

View File

@ -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';
@ -14,6 +14,19 @@ import {
import { ProjectActionsForm } from './ProjectActionsForm/ProjectActionsForm';
import { useProjectActionsForm } from './ProjectActionsForm/useProjectActionsForm';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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',
@ -36,12 +49,14 @@ interface IProjectActionsModalProps {
action?: IActionSet;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onOpenEvents: () => void;
}
export const ProjectActionsModal = ({
action,
open,
setOpen,
onOpenEvents,
}: IProjectActionsModalProps) => {
const projectId = useRequiredPathParam('projectId');
const { refetch } = useActions(projectId);
@ -142,12 +157,18 @@ export const ProjectActionsModal = ({
<FormTemplate
loading={loading}
modal
title={title}
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.'
documentationLink='https://docs.getunleash.io/reference/actions'
documentationLinkLabel='Actions documentation'
formatApiCode={formatApiCode}
>
<StyledHeader>
<StyledTitle>{title}</StyledTitle>
<ConditionallyRender
condition={editing}
show={<Link onClick={onOpenEvents}>View events</Link>}
/>
</StyledHeader>
<StyledForm onSubmit={onSubmit}>
<ProjectActionsForm
enabled={enabled}

View File

@ -24,6 +24,7 @@ import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServ
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { ProjectActionsEventsModal } from './ProjectActionsEventsModal/ProjectActionsEventsModal';
interface IProjectActionsTableProps {
modalOpen: boolean;
@ -49,6 +50,7 @@ export const ProjectActionsTable = ({
const { incomingWebhooks } = useIncomingWebhooks();
const { serviceAccounts } = useServiceAccounts();
const [eventsModalOpen, setEventsModalOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const onToggleAction = async (action: IActionSet, enabled: boolean) => {
@ -182,6 +184,10 @@ export const ProjectActionsTable = ({
}: { row: { original: IActionSet } }) => (
<ProjectActionsTableActionsCell
actionId={action.id}
onOpenEvents={() => {
setSelectedAction(action);
setEventsModalOpen(true);
}}
onEdit={() => {
setSelectedAction(action);
setModalOpen(true);
@ -255,6 +261,19 @@ export const ProjectActionsTable = ({
action={selectedAction}
open={modalOpen}
setOpen={setModalOpen}
onOpenEvents={() => {
setModalOpen(false);
setEventsModalOpen(true);
}}
/>
<ProjectActionsEventsModal
action={selectedAction}
open={eventsModalOpen}
setOpen={setEventsModalOpen}
onOpenConfiguration={() => {
setEventsModalOpen(false);
setModalOpen(true);
}}
/>
<ProjectActionsDeleteDialog
action={selectedAction}

View File

@ -12,7 +12,7 @@ import {
styled,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { Delete, Edit } from '@mui/icons-material';
import { Delete, Edit, Visibility } from '@mui/icons-material';
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { defaultBorderRadius } from 'themes/themeStyles';
@ -24,12 +24,14 @@ const StyledBoxCell = styled(Box)({
interface IProjectActionsTableActionsCellProps {
actionId: number;
onOpenEvents: (event: React.SyntheticEvent) => void;
onEdit: (event: React.SyntheticEvent) => void;
onDelete: (event: React.SyntheticEvent) => void;
}
export const ProjectActionsTableActionsCell = ({
actionId,
onOpenEvents,
onEdit,
onDelete,
}: IProjectActionsTableActionsCellProps) => {
@ -80,6 +82,24 @@ export const ProjectActionsTableActionsCell = ({
}}
>
<MenuList aria-labelledby={id}>
<PermissionHOC permission={ADMIN}>
{({ hasAccess }) => (
<MenuItem
sx={defaultBorderRadius}
onClick={onOpenEvents}
disabled={!hasAccess}
>
<ListItemIcon>
<Visibility />
</ListItemIcon>
<ListItemText>
<Typography variant='body2'>
View events
</Typography>
</ListItemText>
</MenuItem>
)}
</PermissionHOC>
<PermissionHOC permission={ADMIN}>
{({ hasAccess }) => (
<MenuItem

View File

@ -0,0 +1,83 @@
import useSWRInfinite, {
SWRInfiniteConfiguration,
SWRInfiniteKeyLoader,
} from 'swr/infinite';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import useUiConfig from '../useUiConfig/useUiConfig';
import { IActionSetEvent } from 'interfaces/action';
import { useUiFlag } from 'hooks/useUiFlag';
type ActionEventsResponse = {
actionSetEvents: IActionSetEvent[];
};
const fetcher = async (url: string) => {
const response = await fetch(url);
await handleErrorResponses('Action events')(response);
return response.json();
};
export const useActionEvents = (
actionSetId?: number,
projectId?: string,
limit = 50,
options: SWRInfiniteConfiguration = {},
) => {
const { isEnterprise } = useUiConfig();
const automatedActionsEnabled = useUiFlag('automatedActions');
const getKey: SWRInfiniteKeyLoader = (
pageIndex: number,
previousPageData: ActionEventsResponse,
) => {
// Does not meet conditions
if (
!actionSetId ||
!projectId ||
!isEnterprise ||
!automatedActionsEnabled
)
return null;
// Reached the end
if (previousPageData && !previousPageData.actionSetEvents.length)
return null;
return formatApiPath(
`api/admin/projects/${projectId}/actions/${actionSetId}/events?limit=${limit}&offset=${
pageIndex * limit
}`,
);
};
const { data, error, size, setSize, mutate } =
useSWRInfinite<ActionEventsResponse>(getKey, fetcher, {
...options,
revalidateAll: true,
});
const actionEvents = data
? data.flatMap(({ actionSetEvents }) => actionSetEvents)
: [];
const isLoadingInitialData = !data && !error;
const isLoadingMore = size > 0 && !data?.[size - 1];
const loading = isLoadingInitialData || isLoadingMore;
const hasMore = data?.[size - 1]?.actionSetEvents.length === limit;
const loadMore = () => {
if (loading || !hasMore) return;
setSize(size + 1);
};
return {
actionEvents,
hasMore,
loadMore,
loading,
refetch: () => mutate(),
error,
};
};

View File

@ -26,3 +26,37 @@ export interface IAction {
createdAt: string;
createdByUserId: number;
}
export type ObservableEventSource = 'incoming-webhook';
export interface IObservableEvent {
id: number;
source: ObservableEventSource;
sourceId: number;
createdAt: string;
createdByIncomingWebhookTokenId: number;
payload: Record<string, unknown>;
}
type ActionSetState = 'started' | 'success' | 'failed';
type ActionState = ActionSetState | 'not started';
export interface IActionEvent extends IAction {
state: ActionState;
details?: string;
}
interface IActionSetEventActionSet extends IActionSet {
actions: IActionEvent[];
}
export interface IActionSetEvent {
id: number;
actionSetId: number;
observableEventId: number;
createdAt: string;
state: ActionSetState;
observableEvent: IObservableEvent;
actionSet: IActionSetEventActionSet;
}

View File

@ -1,3 +1,5 @@
import { ObservableEventSource } from './action';
export interface IIncomingWebhook {
id: number;
enabled: boolean;
@ -16,13 +18,11 @@ export interface IIncomingWebhookToken {
createdByUserId: number;
}
type EventSource = 'incoming-webhook';
export interface IIncomingWebhookEvent {
id: number;
payload: Record<string, unknown>;
createdAt: string;
source: EventSource;
source: ObservableEventSource;
sourceId: number;
tokenName: string;
}