mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
chore: action events UI (#6358)
https://linear.app/unleash/issue/2-1936/action-events-ui Implements the UI for action events. ![image](https://github.com/Unleash/unleash/assets/14320932/d2f82ddd-6292-4f61-bfdd-05037f746f52) ![image](https://github.com/Unleash/unleash/assets/14320932/673816b8-7dee-4b36-adda-d13d419dc5ac)
This commit is contained in:
parent
9101c39eb7
commit
477a9c6cfa
@ -21,13 +21,14 @@ const StyledSidePanelHalf = styled('div')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const StyledSidePanelHalfLeft = styled(StyledSidePanelHalf, {
|
const StyledSidePanelHalfLeft = styled(StyledSidePanelHalf, {
|
||||||
shouldForwardProp: (prop) => prop !== 'height',
|
shouldForwardProp: (prop) => prop !== 'height' && prop !== 'maxWidth',
|
||||||
})<{ height?: number }>(({ theme, height }) => ({
|
})<{ height?: number; maxWidth?: number }>(({ theme, height, maxWidth }) => ({
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
borderTop: 0,
|
borderTop: 0,
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
...(height && { height }),
|
...(height && { height }),
|
||||||
|
...(maxWidth && { maxWidth }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSidePanelHalfRight = styled(StyledSidePanelHalf)(({ theme }) => ({
|
const StyledSidePanelHalfRight = styled(StyledSidePanelHalf)(({ theme }) => ({
|
||||||
@ -43,12 +44,14 @@ export const StyledSidePanelListColumn = styled('div', {
|
|||||||
shouldForwardProp: (prop) => prop !== 'maxWidth' && prop !== 'align',
|
shouldForwardProp: (prop) => prop !== 'maxWidth' && prop !== 'align',
|
||||||
})<{ maxWidth?: number; align?: ColumnAlignment }>(
|
})<{ maxWidth?: number; align?: ColumnAlignment }>(
|
||||||
({ theme, maxWidth, align = 'start' }) => ({
|
({ theme, maxWidth, align = 'start' }) => ({
|
||||||
|
display: 'flex',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
justifyContent: align,
|
justifyContent: align,
|
||||||
...(maxWidth && { maxWidth }),
|
...(maxWidth && { maxWidth }),
|
||||||
textAlign: align,
|
textAlign: align,
|
||||||
|
alignItems: 'center',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ interface ISidePanelListProps<T> {
|
|||||||
columns: SidePanelListColumn<T>[];
|
columns: SidePanelListColumn<T>[];
|
||||||
sidePanelHeader: string;
|
sidePanelHeader: string;
|
||||||
renderContent: (item: T) => ReactNode;
|
renderContent: (item: T) => ReactNode;
|
||||||
|
renderItem?: (item: T, children: ReactNode) => ReactNode;
|
||||||
height?: number;
|
height?: number;
|
||||||
listEnd?: ReactNode;
|
listEnd?: ReactNode;
|
||||||
}
|
}
|
||||||
@ -73,6 +77,7 @@ export const SidePanelList = <T extends { id: string | number }>({
|
|||||||
columns,
|
columns,
|
||||||
sidePanelHeader,
|
sidePanelHeader,
|
||||||
renderContent,
|
renderContent,
|
||||||
|
renderItem = (_, children) => children,
|
||||||
height,
|
height,
|
||||||
listEnd,
|
listEnd,
|
||||||
}: ISidePanelListProps<T>) => {
|
}: ISidePanelListProps<T>) => {
|
||||||
@ -83,34 +88,44 @@ export const SidePanelList = <T extends { id: string | number }>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeItem = selectedItem || items[0];
|
const activeItem = selectedItem || items[0];
|
||||||
|
const leftPanelMaxWidth = columns.every(({ maxWidth }) => Boolean(maxWidth))
|
||||||
|
? columns.reduce((acc, { maxWidth }) => acc + (maxWidth || 0), 0)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSidePanelListWrapper>
|
<StyledSidePanelListWrapper>
|
||||||
<SidePanelListHeader
|
<SidePanelListHeader
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sidePanelHeader={sidePanelHeader}
|
sidePanelHeader={sidePanelHeader}
|
||||||
|
leftPanelMaxWidth={leftPanelMaxWidth}
|
||||||
/>
|
/>
|
||||||
<StyledSidePanelListBody>
|
<StyledSidePanelListBody>
|
||||||
<StyledSidePanelHalfLeft height={height}>
|
<StyledSidePanelHalfLeft
|
||||||
{items.map((item) => (
|
height={height}
|
||||||
<SidePanelListItem
|
maxWidth={leftPanelMaxWidth}
|
||||||
key={item.id}
|
>
|
||||||
selected={activeItem.id === item.id}
|
{items.map((item) =>
|
||||||
onClick={() => setSelectedItem(item)}
|
renderItem(
|
||||||
>
|
item,
|
||||||
{columns.map(
|
<SidePanelListItem
|
||||||
({ header, maxWidth, align, cell }) => (
|
key={item.id}
|
||||||
<StyledSidePanelListColumn
|
selected={activeItem.id === item.id}
|
||||||
key={header}
|
onClick={() => setSelectedItem(item)}
|
||||||
maxWidth={maxWidth}
|
>
|
||||||
align={align}
|
{columns.map(
|
||||||
>
|
({ header, maxWidth, align, cell }) => (
|
||||||
{cell(item)}
|
<StyledSidePanelListColumn
|
||||||
</StyledSidePanelListColumn>
|
key={header}
|
||||||
),
|
maxWidth={maxWidth}
|
||||||
)}
|
align={align}
|
||||||
</SidePanelListItem>
|
>
|
||||||
))}
|
{cell(item)}
|
||||||
|
</StyledSidePanelListColumn>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</SidePanelListItem>,
|
||||||
|
),
|
||||||
|
)}
|
||||||
{listEnd}
|
{listEnd}
|
||||||
</StyledSidePanelHalfLeft>
|
</StyledSidePanelHalfLeft>
|
||||||
<StyledSidePanelHalfRight>
|
<StyledSidePanelHalfRight>
|
||||||
|
@ -13,22 +13,27 @@ const StyledHeader = styled('div')(({ theme }) => ({
|
|||||||
backgroundColor: theme.palette.table.headerBackground,
|
backgroundColor: theme.palette.table.headerBackground,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderHalf = styled('div')({
|
const StyledHeaderHalf = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'maxWidth',
|
||||||
|
})<{ maxWidth?: number }>(({ maxWidth }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
});
|
...(maxWidth && { maxWidth }),
|
||||||
|
}));
|
||||||
|
|
||||||
interface ISidePanelListHeaderProps<T> {
|
interface ISidePanelListHeaderProps<T> {
|
||||||
columns: SidePanelListColumn<T>[];
|
columns: SidePanelListColumn<T>[];
|
||||||
sidePanelHeader: string;
|
sidePanelHeader: string;
|
||||||
|
leftPanelMaxWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidePanelListHeader = <T,>({
|
export const SidePanelListHeader = <T,>({
|
||||||
columns,
|
columns,
|
||||||
sidePanelHeader,
|
sidePanelHeader,
|
||||||
|
leftPanelMaxWidth,
|
||||||
}: ISidePanelListHeaderProps<T>) => (
|
}: ISidePanelListHeaderProps<T>) => (
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<StyledHeaderHalf>
|
<StyledHeaderHalf maxWidth={leftPanelMaxWidth}>
|
||||||
{columns.map(({ header, maxWidth, align }) => (
|
{columns.map(({ header, maxWidth, align }) => (
|
||||||
<StyledSidePanelListColumn
|
<StyledSidePanelListColumn
|
||||||
key={header}
|
key={header}
|
||||||
|
@ -39,17 +39,17 @@ const StyledItem = styled(Button, {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ISidePanelListItemProps<T> {
|
interface ISidePanelListItemProps {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidePanelListItem = <T,>({
|
export const SidePanelListItem = ({
|
||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
}: ISidePanelListItemProps<T>) => (
|
}: ISidePanelListItemProps) => (
|
||||||
<StyledItemRow>
|
<StyledItemRow>
|
||||||
<StyledItem selected={selected} onClick={onClick}>
|
<StyledItem selected={selected} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -121,6 +121,7 @@ export const IncomingWebhooksEventsModal = ({
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Date',
|
header: 'Date',
|
||||||
|
maxWidth: 180,
|
||||||
cell: (event) =>
|
cell: (event) =>
|
||||||
formatDateYMDHMS(
|
formatDateYMDHMS(
|
||||||
event.createdAt,
|
event.createdAt,
|
||||||
@ -129,6 +130,7 @@ export const IncomingWebhooksEventsModal = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Token',
|
header: 'Token',
|
||||||
|
maxWidth: 350,
|
||||||
cell: (event) => event.tokenName,
|
cell: (event) => event.tokenName,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
TokenGeneration,
|
TokenGeneration,
|
||||||
useIncomingWebhooksForm,
|
useIncomingWebhooksForm,
|
||||||
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
|
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -158,7 +159,10 @@ export const IncomingWebhooksModal = ({
|
|||||||
>
|
>
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<StyledTitle>{title}</StyledTitle>
|
<StyledTitle>{title}</StyledTitle>
|
||||||
<Link onClick={onOpenEvents}>View events</Link>
|
<ConditionallyRender
|
||||||
|
condition={editing}
|
||||||
|
show={<Link onClick={onOpenEvents}>View events</Link>}
|
||||||
|
/>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
<StyledForm onSubmit={onSubmit}>
|
<StyledForm onSubmit={onSubmit}>
|
||||||
<IncomingWebhooksForm
|
<IncomingWebhooksForm
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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;
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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} />;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { FormEvent, useEffect } from 'react';
|
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 { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
@ -14,6 +14,19 @@ import {
|
|||||||
import { ProjectActionsForm } from './ProjectActionsForm/ProjectActionsForm';
|
import { ProjectActionsForm } from './ProjectActionsForm/ProjectActionsForm';
|
||||||
import { useProjectActionsForm } from './ProjectActionsForm/useProjectActionsForm';
|
import { useProjectActionsForm } from './ProjectActionsForm/useProjectActionsForm';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
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')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -36,12 +49,14 @@ interface IProjectActionsModalProps {
|
|||||||
action?: IActionSet;
|
action?: IActionSet;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
onOpenEvents: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsModal = ({
|
export const ProjectActionsModal = ({
|
||||||
action,
|
action,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
onOpenEvents,
|
||||||
}: IProjectActionsModalProps) => {
|
}: IProjectActionsModalProps) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { refetch } = useActions(projectId);
|
const { refetch } = useActions(projectId);
|
||||||
@ -142,12 +157,18 @@ export const ProjectActionsModal = ({
|
|||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
modal
|
modal
|
||||||
title={title}
|
|
||||||
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.'
|
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.'
|
||||||
documentationLink='https://docs.getunleash.io/reference/actions'
|
documentationLink='https://docs.getunleash.io/reference/actions'
|
||||||
documentationLinkLabel='Actions documentation'
|
documentationLinkLabel='Actions documentation'
|
||||||
formatApiCode={formatApiCode}
|
formatApiCode={formatApiCode}
|
||||||
>
|
>
|
||||||
|
<StyledHeader>
|
||||||
|
<StyledTitle>{title}</StyledTitle>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={editing}
|
||||||
|
show={<Link onClick={onOpenEvents}>View events</Link>}
|
||||||
|
/>
|
||||||
|
</StyledHeader>
|
||||||
<StyledForm onSubmit={onSubmit}>
|
<StyledForm onSubmit={onSubmit}>
|
||||||
<ProjectActionsForm
|
<ProjectActionsForm
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
|
@ -24,6 +24,7 @@ import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServ
|
|||||||
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
import { ProjectActionsEventsModal } from './ProjectActionsEventsModal/ProjectActionsEventsModal';
|
||||||
|
|
||||||
interface IProjectActionsTableProps {
|
interface IProjectActionsTableProps {
|
||||||
modalOpen: boolean;
|
modalOpen: boolean;
|
||||||
@ -49,6 +50,7 @@ export const ProjectActionsTable = ({
|
|||||||
const { incomingWebhooks } = useIncomingWebhooks();
|
const { incomingWebhooks } = useIncomingWebhooks();
|
||||||
const { serviceAccounts } = useServiceAccounts();
|
const { serviceAccounts } = useServiceAccounts();
|
||||||
|
|
||||||
|
const [eventsModalOpen, setEventsModalOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
|
||||||
const onToggleAction = async (action: IActionSet, enabled: boolean) => {
|
const onToggleAction = async (action: IActionSet, enabled: boolean) => {
|
||||||
@ -182,6 +184,10 @@ export const ProjectActionsTable = ({
|
|||||||
}: { row: { original: IActionSet } }) => (
|
}: { row: { original: IActionSet } }) => (
|
||||||
<ProjectActionsTableActionsCell
|
<ProjectActionsTableActionsCell
|
||||||
actionId={action.id}
|
actionId={action.id}
|
||||||
|
onOpenEvents={() => {
|
||||||
|
setSelectedAction(action);
|
||||||
|
setEventsModalOpen(true);
|
||||||
|
}}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setSelectedAction(action);
|
setSelectedAction(action);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@ -255,6 +261,19 @@ export const ProjectActionsTable = ({
|
|||||||
action={selectedAction}
|
action={selectedAction}
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
setOpen={setModalOpen}
|
setOpen={setModalOpen}
|
||||||
|
onOpenEvents={() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
setEventsModalOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProjectActionsEventsModal
|
||||||
|
action={selectedAction}
|
||||||
|
open={eventsModalOpen}
|
||||||
|
setOpen={setEventsModalOpen}
|
||||||
|
onOpenConfiguration={() => {
|
||||||
|
setEventsModalOpen(false);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ProjectActionsDeleteDialog
|
<ProjectActionsDeleteDialog
|
||||||
action={selectedAction}
|
action={selectedAction}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
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 { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { defaultBorderRadius } from 'themes/themeStyles';
|
import { defaultBorderRadius } from 'themes/themeStyles';
|
||||||
@ -24,12 +24,14 @@ const StyledBoxCell = styled(Box)({
|
|||||||
|
|
||||||
interface IProjectActionsTableActionsCellProps {
|
interface IProjectActionsTableActionsCellProps {
|
||||||
actionId: number;
|
actionId: number;
|
||||||
|
onOpenEvents: (event: React.SyntheticEvent) => void;
|
||||||
onEdit: (event: React.SyntheticEvent) => void;
|
onEdit: (event: React.SyntheticEvent) => void;
|
||||||
onDelete: (event: React.SyntheticEvent) => void;
|
onDelete: (event: React.SyntheticEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectActionsTableActionsCell = ({
|
export const ProjectActionsTableActionsCell = ({
|
||||||
actionId,
|
actionId,
|
||||||
|
onOpenEvents,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: IProjectActionsTableActionsCellProps) => {
|
}: IProjectActionsTableActionsCellProps) => {
|
||||||
@ -80,6 +82,24 @@ export const ProjectActionsTableActionsCell = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuList aria-labelledby={id}>
|
<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}>
|
<PermissionHOC permission={ADMIN}>
|
||||||
{({ hasAccess }) => (
|
{({ hasAccess }) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -26,3 +26,37 @@ export interface IAction {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
createdByUserId: number;
|
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;
|
||||||
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ObservableEventSource } from './action';
|
||||||
|
|
||||||
export interface IIncomingWebhook {
|
export interface IIncomingWebhook {
|
||||||
id: number;
|
id: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -16,13 +18,11 @@ export interface IIncomingWebhookToken {
|
|||||||
createdByUserId: number;
|
createdByUserId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventSource = 'incoming-webhook';
|
|
||||||
|
|
||||||
export interface IIncomingWebhookEvent {
|
export interface IIncomingWebhookEvent {
|
||||||
id: number;
|
id: number;
|
||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
source: EventSource;
|
source: ObservableEventSource;
|
||||||
sourceId: number;
|
sourceId: number;
|
||||||
tokenName: string;
|
tokenName: string;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user