1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

chore: rename incoming webhooks to signals (#6415)

https://linear.app/unleash/issue/2-1994/ui-feature-rename-adapt-the-signals-ui
https://linear.app/unleash/issue/2-1996/rename-feature-in-the-code-base

Implements the feature rename to Signals by adapting the code base and
UI.
This commit is contained in:
Nuno Góis 2024-03-04 12:08:05 +00:00 committed by GitHub
parent 4fc0a806f1
commit 68729333e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 777 additions and 875 deletions

View File

@ -108,10 +108,10 @@ const PremiumFeatures = {
url: 'https://docs.getunleash.io/reference/banners', url: 'https://docs.getunleash.io/reference/banners',
label: 'Banners', label: 'Banners',
}, },
'incoming-webhooks': { signals: {
plan: FeaturePlan.ENTERPRISE, plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/incoming-webhooks', url: 'https://docs.getunleash.io/reference/signals',
label: 'Incoming Webhooks', label: 'Signals',
}, },
actions: { actions: {
plan: FeaturePlan.ENTERPRISE, plan: FeaturePlan.ENTERPRISE,

View File

@ -1,41 +0,0 @@
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { IncomingWebhooksTable } from './IncomingWebhooksTable/IncomingWebhooksTable';
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
interface IIncomingWebhooksProps {
modalOpen: boolean;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
selectedIncomingWebhook?: IIncomingWebhook;
setSelectedIncomingWebhook: React.Dispatch<
React.SetStateAction<IIncomingWebhook | undefined>
>;
}
export const IncomingWebhooks = ({
modalOpen,
setModalOpen,
selectedIncomingWebhook,
setSelectedIncomingWebhook,
}: IIncomingWebhooksProps) => {
const { isEnterprise } = useUiConfig();
if (!isEnterprise()) {
return <PremiumFeature feature='incoming-webhooks' />;
}
return (
<div>
<PermissionGuard permissions={ADMIN}>
<IncomingWebhooksTable
modalOpen={modalOpen}
setModalOpen={setModalOpen}
selectedIncomingWebhook={selectedIncomingWebhook}
setSelectedIncomingWebhook={setSelectedIncomingWebhook}
/>
</PermissionGuard>
</div>
);
};

View File

@ -1,32 +0,0 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
interface IIncomingWebhooksDeleteDialogProps {
incomingWebhook?: IIncomingWebhook;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: (incomingWebhook: IIncomingWebhook) => void;
}
export const IncomingWebhooksDeleteDialog = ({
incomingWebhook,
open,
setOpen,
onConfirm,
}: IIncomingWebhooksDeleteDialogProps) => (
<Dialogue
title='Delete incoming webhook?'
open={open}
primaryButtonText='Delete incoming webhook'
secondaryButtonText='Cancel'
onClick={() => onConfirm(incomingWebhook!)}
onClose={() => {
setOpen(false);
}}
>
<p>
You are about to delete incoming webhook:{' '}
<strong>{incomingWebhook?.name}</strong>
</p>
</Dialogue>
);

View File

@ -8,10 +8,10 @@ import { RequestIntegrationCard } from '../RequestIntegrationCard/RequestIntegra
import { OFFICIAL_SDKS } from './SDKs'; import { OFFICIAL_SDKS } from './SDKs';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IAvailableIntegrationsProps { interface IAvailableIntegrationsProps {
providers: AddonTypeSchema[]; providers: AddonTypeSchema[];
onNewIncomingWebhook: () => void;
} }
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -53,9 +53,9 @@ const StyledGrayContainer = styled('div')(({ theme }) => ({
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({ export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
providers, providers,
onNewIncomingWebhook,
}) => { }) => {
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks'); const { isEnterprise } = useUiConfig();
const signalsEnabled = useUiFlag('signals');
const customProviders = [JIRA_INFO]; const customProviders = [JIRA_INFO];
const serverSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'server'); const serverSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'server');
@ -98,13 +98,13 @@ export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
), ),
)} )}
<ConditionallyRender <ConditionallyRender
condition={incomingWebhooksEnabled} condition={isEnterprise() && signalsEnabled}
show={ show={
<IntegrationCard <IntegrationCard
icon='webhook' icon='webhook'
title='Incoming Webhooks' title='Signals'
description='Incoming Webhooks allow third-party services to send observable events to Unleash.' description='Signal endpoints allow third-party services to send signals to Unleash.'
onClick={onNewIncomingWebhook} link='/integrations/signals'
/> />
} }
/> />

View File

@ -4,6 +4,10 @@ import { StyledCardsGrid } from '../IntegrationList.styles';
import { IntegrationCard } from '../IntegrationCard/IntegrationCard'; import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
import { VFC } from 'react'; import { VFC } from 'react';
import { Typography, styled } from '@mui/material'; import { Typography, styled } from '@mui/material';
import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { useUiFlag } from 'hooks/useUiFlag';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledConfiguredSection = styled('section')(({ theme }) => ({ const StyledConfiguredSection = styled('section')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -23,6 +27,10 @@ export const ConfiguredIntegrations: VFC<ConfiguredIntegrationsProps> = ({
addons, addons,
providers, providers,
}) => { }) => {
const { signalEndpoints } = useSignalEndpoints();
const signalsEnabled = useUiFlag('signals');
const { isEnterprise } = useUiConfig();
const ref = useLoading(loading || false); const ref = useLoading(loading || false);
return ( return (
@ -65,6 +73,26 @@ export const ConfiguredIntegrations: VFC<ConfiguredIntegrationsProps> = ({
/> />
); );
})} })}
<ConditionallyRender
condition={
isEnterprise() && signalsEnabled
// && signalEndpoints.length > 0
}
show={
<IntegrationCard
variant='stacked'
icon='webhook'
title='Signals'
description={`${
signalEndpoints.length
} signal endpoint${
signalEndpoints.length === 1 ? '' : 's'
} configured`}
link='/integrations/signals'
configureActionText='View signal endpoints'
/>
}
/>
</StyledCardsGrid> </StyledCardsGrid>
</StyledConfiguredSection> </StyledConfiguredSection>
); );

View File

@ -10,7 +10,10 @@ import type { AddonSchema } from 'openapi';
import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
type CardVariant = 'default' | 'stacked';
interface IIntegrationCardBaseProps { interface IIntegrationCardBaseProps {
variant?: CardVariant;
id?: string | number; id?: string | number;
icon?: string; icon?: string;
title: string; title: string;
@ -37,7 +40,9 @@ type IIntegrationCardProps =
| IIntegrationCardWithLinkProps | IIntegrationCardWithLinkProps
| IIntegrationCardWithOnClickProps; | IIntegrationCardWithOnClickProps;
const StyledCard = styled('div')(({ theme }) => ({ const StyledCard = styled('div', {
shouldForwardProp: (prop) => prop !== 'variant',
})<{ variant?: CardVariant }>(({ theme, variant = 'default' }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: theme.spacing(3), padding: theme.spacing(3),
@ -48,6 +53,24 @@ const StyledCard = styled('div')(({ theme }) => ({
':hover': { ':hover': {
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
}, },
...(variant === 'stacked' && {
position: 'relative',
zIndex: 0,
'&:after': {
content: '""',
width: 'auto',
height: theme.spacing(0.75),
position: 'absolute',
zIndex: -1,
bottom: theme.spacing(-0.75),
left: theme.spacing(1),
right: theme.spacing(1),
borderBottomLeftRadius: `${theme.shape.borderRadiusMedium}px`,
borderBottomRightRadius: `${theme.shape.borderRadiusMedium}px`,
border: `1px solid ${theme.palette.divider}`,
boxShadow: theme.boxShadows.card,
},
}),
})); }));
const StyledLink = styled(Link)({ const StyledLink = styled(Link)({
@ -89,6 +112,7 @@ const StyledOpenInNewIcon = styled(OpenInNewIcon)(({ theme }) => ({
})); }));
export const IntegrationCard: VFC<IIntegrationCardProps> = ({ export const IntegrationCard: VFC<IIntegrationCardProps> = ({
variant = 'default',
icon, icon,
title, title,
description, description,
@ -113,7 +137,7 @@ export const IntegrationCard: VFC<IIntegrationCardProps> = ({
}; };
const content = ( const content = (
<StyledCard> <StyledCard variant={variant}>
<StyledHeader> <StyledHeader>
<StyledTitle variant='h3' data-loading> <StyledTitle variant='h3' data-loading>
<IntegrationIcon name={icon as string} /> {title} <IntegrationIcon name={icon as string} /> {title}

View File

@ -1,162 +1,32 @@
import { VFC, useState } from 'react'; import { VFC } from 'react';
import useAddons from 'hooks/api/getters/useAddons/useAddons'; import useAddons from 'hooks/api/getters/useAddons/useAddons';
import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations'; import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations';
import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations'; import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations';
import { Tab, Tabs, styled, useTheme } from '@mui/material';
import { Add } from '@mui/icons-material';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { useUiFlag } from 'hooks/useUiFlag';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { TabLink } from 'component/common/TabNav/TabLink'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
import { IncomingWebhooks } from 'component/incomingWebhooks/IncomingWebhooks';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledHeader = styled('div')(() => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}));
const StyledTabsContainer = styled('div')({
flex: 1,
});
const StyledActions = styled('div')({
display: 'flex',
alignItems: 'center',
});
export const IntegrationList: VFC = () => { export const IntegrationList: VFC = () => {
const { isEnterprise } = useUiConfig(); const { signalEndpoints } = useSignalEndpoints();
const { pathname } = useLocation();
const navigate = useNavigate();
const theme = useTheme();
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
const { providers, addons, loading } = useAddons(); const { providers, addons, loading } = useAddons();
const { incomingWebhooks } = useIncomingWebhooks();
const [selectedIncomingWebhook, setSelectedIncomingWebhook] =
useState<IIncomingWebhook>();
const [incomingWebhookModalOpen, setIncomingWebhookModalOpen] =
useState(false);
const onNewIncomingWebhook = () => {
navigate('/integrations/incoming-webhooks');
setSelectedIncomingWebhook(undefined);
setIncomingWebhookModalOpen(true);
};
const tabs = [
{
label: 'Integrations',
path: '/integrations',
},
{
label: `Incoming webhooks (${incomingWebhooks.length})`,
path: '/integrations/incoming-webhooks',
},
];
return ( return (
<PageContent <PageContent
header={ header={<PageHeader title='Integrations' />}
<ConditionallyRender
condition={incomingWebhooksEnabled}
show={
<StyledHeader>
<StyledTabsContainer>
<Tabs
value={pathname}
indicatorColor='primary'
textColor='primary'
variant='scrollable'
allowScrollButtonsMobile
>
{tabs.map(({ label, path }) => (
<Tab
key={label}
value={path}
label={
<TabLink to={path}>
{label}
</TabLink>
}
sx={{
padding: 0,
}}
/>
))}
</Tabs>
</StyledTabsContainer>
<StyledActions>
<ConditionallyRender
condition={
pathname.includes(
'incoming-webhooks',
) && isEnterprise()
}
show={
<ResponsiveButton
onClick={onNewIncomingWebhook}
maxWidth={`${theme.breakpoints.values.sm}px`}
Icon={Add}
permission={ADMIN}
>
New incoming webhook
</ResponsiveButton>
}
/>
</StyledActions>
</StyledHeader>
}
elseShow={<PageHeader title='Integrations' />}
/>
}
isLoading={loading} isLoading={loading}
withTabs={incomingWebhooksEnabled}
> >
<Routes> <ConditionallyRender
<Route condition={addons.length > 0 || signalEndpoints.length > 0}
path='incoming-webhooks' show={
element={ <ConfiguredIntegrations
<IncomingWebhooks addons={addons}
modalOpen={incomingWebhookModalOpen} providers={providers}
setModalOpen={setIncomingWebhookModalOpen} loading={loading}
selectedIncomingWebhook={selectedIncomingWebhook} />
setSelectedIncomingWebhook={ }
setSelectedIncomingWebhook />
} <AvailableIntegrations providers={providers} />
/>
}
/>
<Route
path='*'
element={
<>
<ConditionallyRender
condition={addons.length > 0}
show={
<ConfiguredIntegrations
addons={addons}
providers={providers}
loading={loading}
/>
}
/>
<AvailableIntegrations
providers={providers}
onNewIncomingWebhook={onNewIncomingWebhook}
/>
</>
}
/>
</Routes>
</PageContent> </PageContent>
); );
}; };

View File

@ -343,7 +343,7 @@ exports[`returns all baseRoutes 1`] = `
"advanced": true, "advanced": true,
"mobile": true, "mobile": true,
}, },
"path": "/integrations/*", "path": "/integrations",
"title": "Integrations", "title": "Integrations",
"type": "protected", "type": "protected",
}, },

View File

@ -47,6 +47,7 @@ import { AddonRedirect } from 'component/integrations/AddonRedirect/AddonRedirec
import { ExecutiveDashboard } from 'component/executiveDashboard/ExecutiveDashboard'; import { ExecutiveDashboard } from 'component/executiveDashboard/ExecutiveDashboard';
import { FeedbackList } from '../feedbackNew/FeedbackList'; import { FeedbackList } from '../feedbackNew/FeedbackList';
import { Application } from 'component/application/Application'; import { Application } from 'component/application/Application';
import { Signals } from 'component/signals/Signals';
export const routes: IRoute[] = [ export const routes: IRoute[] = [
// Splash // Splash
@ -351,13 +352,21 @@ export const routes: IRoute[] = [
menu: {}, menu: {},
}, },
{ {
path: '/integrations/*', path: '/integrations',
title: 'Integrations', title: 'Integrations',
component: IntegrationList, component: IntegrationList,
hidden: false, hidden: false,
type: 'protected', type: 'protected',
menu: { mobile: true, advanced: true }, menu: { mobile: true, advanced: true },
}, },
{
path: '/integrations/signals',
title: 'Signals',
component: Signals,
hidden: true,
type: 'protected',
menu: {},
},
// Segments // Segments
{ {

View File

@ -1,22 +0,0 @@
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

@ -13,7 +13,7 @@ const StyledDetails = styled('div')(({ theme }) => ({
export const ProjectActionsEventsDetails = ({ export const ProjectActionsEventsDetails = ({
state, state,
actionSet: { actions }, actionSet: { actions },
observableEvent, signal,
}: IActionSetEvent) => { }: IActionSetEvent) => {
const stateText = const stateText =
state === 'failed' state === 'failed'
@ -27,9 +27,7 @@ export const ProjectActionsEventsDetails = ({
<Alert severity={state === 'failed' ? 'error' : 'success'}> <Alert severity={state === 'failed' ? 'error' : 'success'}>
{stateText} {stateText}
</Alert> </Alert>
<ProjectActionsEventsDetailsSource <ProjectActionsEventsDetailsSource signal={signal} />
observableEvent={observableEvent}
/>
{actions.map((action, i) => ( {actions.map((action, i) => (
<ProjectActionsEventsDetailsAction <ProjectActionsEventsDetailsAction
key={action.id} key={action.id}

View File

@ -0,0 +1,20 @@
import { ISignal } from 'interfaces/signal';
import { ProjectActionsEventsDetailsSourceSignalEndpoint } from './ProjectActionsEventsDetailsSourceSignalEndpoint';
interface IProjectActionsEventsDetailsSourceProps {
signal: ISignal;
}
export const ProjectActionsEventsDetailsSource = ({
signal,
}: IProjectActionsEventsDetailsSourceProps) => {
const { source } = signal;
if (source === 'signal-endpoint') {
return (
<ProjectActionsEventsDetailsSourceSignalEndpoint signal={signal} />
);
}
return null;
};

View File

@ -6,8 +6,8 @@ import {
IconButton, IconButton,
styled, styled,
} from '@mui/material'; } from '@mui/material';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { IObservableEvent } from 'interfaces/action'; import { ISignal } from 'interfaces/signal';
import { Suspense, lazy, useMemo } from 'react'; import { Suspense, lazy, useMemo } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -28,23 +28,22 @@ const StyledLink = styled(Link)(({ theme }) => ({
marginLeft: theme.spacing(1), marginLeft: theme.spacing(1),
})); }));
interface IProjectActionsEventsDetailsSourceIncomingWebhookProps { interface IProjectActionsEventsDetailsSourceSignalEndpointProps {
observableEvent: IObservableEvent; signal: ISignal;
} }
export const ProjectActionsEventsDetailsSourceIncomingWebhook = ({ export const ProjectActionsEventsDetailsSourceSignalEndpoint = ({
observableEvent, signal,
}: IProjectActionsEventsDetailsSourceIncomingWebhookProps) => { }: IProjectActionsEventsDetailsSourceSignalEndpointProps) => {
const { incomingWebhooks } = useIncomingWebhooks(); const { signalEndpoints } = useSignalEndpoints();
const incomingWebhookName = useMemo(() => { const signalEndpointName = useMemo(() => {
const incomingWebhook = incomingWebhooks.find( const signalEndpoint = signalEndpoints.find(
(incomingWebhook) => ({ id }) => id === signal.sourceId,
incomingWebhook.id === observableEvent.sourceId,
); );
return incomingWebhook?.name; return signalEndpoint?.name;
}, [incomingWebhooks, observableEvent.sourceId]); }, [signalEndpoints, signal.sourceId]);
return ( return (
<StyledAccordion> <StyledAccordion>
@ -55,15 +54,15 @@ export const ProjectActionsEventsDetailsSourceIncomingWebhook = ({
</IconButton> </IconButton>
} }
> >
Incoming webhook: Signal endpoint:
<StyledLink to='/integrations/incoming-webhooks'> <StyledLink to='/integrations/signals'>
{incomingWebhookName} {signalEndpointName}
</StyledLink> </StyledLink>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Suspense fallback={null}> <Suspense fallback={null}>
<LazyReactJSONEditor <LazyReactJSONEditor
content={{ json: observableEvent.payload }} content={{ json: signal.payload }}
readOnly readOnly
statusBar={false} statusBar={false}
editorStyle='sidePanel' editorStyle='sidePanel'

View File

@ -9,7 +9,7 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ProjectActionsEventsStateCell } from './ProjectActionsEventsStateCell'; import { ProjectActionsEventsStateCell } from './ProjectActionsEventsStateCell';
import { ProjectActionsEventsDetails } from './ProjectActionsEventsDetails.tsx/ProjectActionsEventsDetails'; import { ProjectActionsEventsDetails } from './ProjectActionsEventsDetails/ProjectActionsEventsDetails';
const StyledHeader = styled('div')(({ theme }) => ({ const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -86,9 +86,9 @@ export const ProjectActionsEventsModal = ({
<FormTemplate <FormTemplate
loading={loading && actionEvents.length === 0} loading={loading && actionEvents.length === 0}
modal modal
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.' description=''
documentationLink='https://docs.getunleash.io/reference/actions' documentationLink=''
documentationLinkLabel='Actions documentation' documentationLinkLabel=''
showGuidance={false} showGuidance={false}
> >
<StyledHeader> <StyledHeader>

View File

@ -9,7 +9,7 @@ import {
ProjectActionsFormErrors, ProjectActionsFormErrors,
} from './useProjectActionsForm'; } from './useProjectActionsForm';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts'; import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useMemo } from 'react'; import { useMemo } from 'react';
import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect, {} from 'component/common/GeneralSelect/GeneralSelect';
@ -100,8 +100,8 @@ export const ProjectActionsForm = ({
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { serviceAccounts, loading: serviceAccountsLoading } = const { serviceAccounts, loading: serviceAccountsLoading } =
useServiceAccounts(); useServiceAccounts();
const { incomingWebhooks, loading: incomingWebhooksLoading } = const { signalEndpoints, loading: signalEndpointsLoading } =
useIncomingWebhooks(); useSignalEndpoints();
const handleOnBlur = (callback: Function) => { const handleOnBlur = (callback: Function) => {
setTimeout(() => callback(), 300); setTimeout(() => callback(), 300);
@ -151,16 +151,16 @@ export const ProjectActionsForm = ({
); );
}; };
const incomingWebhookOptions = useMemo(() => { const signalEndpointOptions = useMemo(() => {
if (incomingWebhooksLoading) { if (signalEndpointsLoading) {
return []; return [];
} }
return incomingWebhooks.map((webhook) => ({ return signalEndpoints.map(({ id, name }) => ({
label: webhook.name, label: name,
key: `${webhook.id}`, key: `${id}`,
})); }));
}, [incomingWebhooksLoading, incomingWebhooks]); }, [signalEndpointsLoading, signalEndpoints]);
const serviceAccountOptions = useMemo(() => { const serviceAccountOptions = useMemo(() => {
if (serviceAccountsLoading) { if (serviceAccountsLoading) {
@ -218,15 +218,14 @@ export const ProjectActionsForm = ({
<ProjectActionsFormStep <ProjectActionsFormStep
name='Trigger' name='Trigger'
resourceLink={ resourceLink={
<RouterLink to='/integrations/incoming-webhooks'> <RouterLink to='/integrations/signals'>
Create incoming webhook Create signal endpoint
</RouterLink> </RouterLink>
} }
> >
<GeneralSelect <GeneralSelect
label='Incoming webhook' label='Source'
name='incoming-webhook' options={signalEndpointOptions}
options={incomingWebhookOptions}
value={`${sourceId}`} value={`${sourceId}`}
onChange={(v) => { onChange={(v) => {
setSourceId(parseInt(v)); setSourceId(parseInt(v));

View File

@ -6,7 +6,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
enum ErrorField { enum ErrorField {
NAME = 'name', NAME = 'name',
TRIGGER = 'trigger', SOURCE = 'source',
FILTERS = 'filters', FILTERS = 'filters',
ACTOR = 'actor', ACTOR = 'actor',
ACTIONS = 'actions', ACTIONS = 'actions',
@ -27,7 +27,7 @@ export type ActionsActionState = Omit<
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = { const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
[ErrorField.NAME]: undefined, [ErrorField.NAME]: undefined,
[ErrorField.TRIGGER]: undefined, [ErrorField.SOURCE]: undefined,
[ErrorField.FILTERS]: undefined, [ErrorField.FILTERS]: undefined,
[ErrorField.ACTOR]: undefined, [ErrorField.ACTOR]: undefined,
[ErrorField.ACTIONS]: undefined, [ErrorField.ACTIONS]: undefined,
@ -127,11 +127,11 @@ export const useProjectActionsForm = (action?: IActionSet) => {
const validateSourceId = (sourceId: number) => { const validateSourceId = (sourceId: number) => {
if (isIdEmpty(sourceId)) { if (isIdEmpty(sourceId)) {
setError(ErrorField.TRIGGER, 'Incoming webhook is required.'); setError(ErrorField.SOURCE, 'Source is required.');
return false; return false;
} }
clearError(ErrorField.TRIGGER); clearError(ErrorField.SOURCE);
return true; return true;
}; };

View File

@ -95,7 +95,7 @@ export const ProjectActionsModal = ({
enabled, enabled,
name, name,
match: { match: {
source: 'incoming-webhook', source: 'signal-endpoint',
sourceId, sourceId,
payload: filters payload: filters
.filter((f) => f.parameter.length > 0) .filter((f) => f.parameter.length > 0)
@ -173,7 +173,7 @@ export const ProjectActionsModal = ({
<FormTemplate <FormTemplate
loading={loading} loading={loading}
modal modal
description='Actions allow you to configure automations based on specific triggers, like incoming webhooks.' description='Actions allow you to configure automations based on specific signals, like the ones originated from signal endpoints.'
documentationLink='https://docs.getunleash.io/reference/actions' documentationLink='https://docs.getunleash.io/reference/actions'
documentationLinkLabel='Actions documentation' documentationLinkLabel='Actions documentation'
formatApiCode={formatApiCode} formatApiCode={formatApiCode}

View File

@ -21,7 +21,7 @@ import { ProjectActionsTableActionsCell } from './ProjectActionsTableActionsCell
import { ProjectActionsModal } from './ProjectActionsModal/ProjectActionsModal'; import { ProjectActionsModal } from './ProjectActionsModal/ProjectActionsModal';
import { ProjectActionsDeleteDialog } from './ProjectActionsDeleteDialog'; import { ProjectActionsDeleteDialog } from './ProjectActionsDeleteDialog';
import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts'; import { useServiceAccounts } from 'hooks/api/getters/useServiceAccounts/useServiceAccounts';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
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'; import { ProjectActionsEventsModal } from './ProjectActionsEventsModal/ProjectActionsEventsModal';
@ -47,7 +47,7 @@ export const ProjectActionsTable = ({
const { actions, refetch } = useActions(projectId); const { actions, refetch } = useActions(projectId);
const { toggleActionSet, removeActionSet } = useActionsApi(projectId); const { toggleActionSet, removeActionSet } = useActionsApi(projectId);
const { incomingWebhooks } = useIncomingWebhooks(); const { signalEndpoints } = useSignalEndpoints();
const { serviceAccounts } = useServiceAccounts(); const { serviceAccounts } = useServiceAccounts();
const [eventsModalOpen, setEventsModalOpen] = useState(false); const [eventsModalOpen, setEventsModalOpen] = useState(false);
@ -111,7 +111,7 @@ export const ProjectActionsTable = ({
}: { row: { original: IActionSet } }) => ( }: { row: { original: IActionSet } }) => (
<ProjectActionsTriggerCell <ProjectActionsTriggerCell
action={action} action={action}
incomingWebhooks={incomingWebhooks} signalEndpoints={signalEndpoints}
/> />
), ),
}, },
@ -202,7 +202,7 @@ export const ProjectActionsTable = ({
disableSortBy: true, disableSortBy: true,
}, },
], ],
[incomingWebhooks, serviceAccounts], [signalEndpoints, serviceAccounts],
); );
const [initialState] = useState({ const [initialState] = useState({

View File

@ -1,7 +1,7 @@
import { Avatar, Box, Link, styled } from '@mui/material'; import { Avatar, Box, Link, styled } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { IActionSet } from 'interfaces/action'; import { IActionSet } from 'interfaces/action';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import webhooksIcon from 'assets/icons/webhooks.svg'; import webhooksIcon from 'assets/icons/webhooks.svg';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { ComponentType } from 'react'; import { ComponentType } from 'react';
@ -32,15 +32,15 @@ const StyledLink = styled(Link)<{
interface IProjectActionsTriggerCellProps { interface IProjectActionsTriggerCellProps {
action: IActionSet; action: IActionSet;
incomingWebhooks: IIncomingWebhook[]; signalEndpoints: ISignalEndpoint[];
} }
export const ProjectActionsTriggerCell = ({ export const ProjectActionsTriggerCell = ({
action, action,
incomingWebhooks, signalEndpoints,
}: IProjectActionsTriggerCellProps) => { }: IProjectActionsTriggerCellProps) => {
const { sourceId } = action.match; const { sourceId } = action.match;
const trigger = incomingWebhooks.find(({ id }) => id === sourceId); const trigger = signalEndpoints.find(({ id }) => id === sourceId);
if (!trigger) { if (!trigger) {
return <TextCell>No trigger</TextCell>; return <TextCell>No trigger</TextCell>;
@ -51,12 +51,12 @@ export const ProjectActionsTriggerCell = ({
<StyledCell> <StyledCell>
<StyledIcon <StyledIcon
src={formatAssetPath(webhooksIcon)} src={formatAssetPath(webhooksIcon)}
alt='Incoming webhook' alt='Signal endpoint'
variant='rounded' variant='rounded'
/> />
<StyledLink <StyledLink
component={RouterLink} component={RouterLink}
to='/integrations/incoming-webhooks' to='/integrations/signals'
underline='hover' underline='hover'
> >
{trigger.name} {trigger.name}

View File

@ -10,13 +10,13 @@ import {
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch'; import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { import {
IncomingWebhooksFormErrors, SignalEndpointsFormErrors,
TokenGeneration, TokenGeneration,
} from './useIncomingWebhooksForm'; } from './useSignalEndpointsForm';
import { IncomingWebhooksFormURL } from './IncomingWebhooksFormURL'; import { SignalEndpointsFormURL } from './SignalEndpointsFormURL';
import { IncomingWebhooksTokens } from './IncomingWebhooksTokens/IncomingWebhooksTokens'; import { SignalEndpointsTokens } from './SignalEndpointsTokens/SignalEndpointsTokens';
const StyledRaisedSection = styled('div')(({ theme }) => ({ const StyledRaisedSection = styled('div')(({ theme }) => ({
background: theme.palette.background.elevation1, background: theme.palette.background.elevation1,
@ -59,8 +59,8 @@ const StyledInlineContainer = styled('div')(({ theme }) => ({
}, },
})); }));
interface IIncomingWebhooksFormProps { interface ISignalEndpointsFormProps {
incomingWebhook?: IIncomingWebhook; signalEndpoint?: ISignalEndpoint;
enabled: boolean; enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>; setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
name: string; name: string;
@ -71,7 +71,7 @@ interface IIncomingWebhooksFormProps {
setTokenGeneration: React.Dispatch<React.SetStateAction<TokenGeneration>>; setTokenGeneration: React.Dispatch<React.SetStateAction<TokenGeneration>>;
tokenName: string; tokenName: string;
setTokenName: React.Dispatch<React.SetStateAction<string>>; setTokenName: React.Dispatch<React.SetStateAction<string>>;
errors: IncomingWebhooksFormErrors; errors: SignalEndpointsFormErrors;
validateName: (name: string) => boolean; validateName: (name: string) => boolean;
validateTokenName: ( validateTokenName: (
tokenGeneration: TokenGeneration, tokenGeneration: TokenGeneration,
@ -80,8 +80,8 @@ interface IIncomingWebhooksFormProps {
validated: boolean; validated: boolean;
} }
export const IncomingWebhooksForm = ({ export const SignalEndpointsForm = ({
incomingWebhook, signalEndpoint,
enabled, enabled,
setEnabled, setEnabled,
name, name,
@ -96,7 +96,7 @@ export const IncomingWebhooksForm = ({
validateName, validateName,
validateTokenName, validateTokenName,
validated, validated,
}: IIncomingWebhooksFormProps) => { }: ISignalEndpointsFormProps) => {
const handleOnBlur = (callback: Function) => { const handleOnBlur = (callback: Function) => {
setTimeout(() => callback(), 300); setTimeout(() => callback(), 300);
}; };
@ -107,15 +107,15 @@ export const IncomingWebhooksForm = ({
<div> <div>
<StyledRaisedSection> <StyledRaisedSection>
<FormSwitch checked={enabled} setChecked={setEnabled}> <FormSwitch checked={enabled} setChecked={setEnabled}>
Incoming webhook status Signal endpoint status
</FormSwitch> </FormSwitch>
</StyledRaisedSection> </StyledRaisedSection>
<StyledInputDescription> <StyledInputDescription>
What is your new incoming webhook name? What is your new signal endpoint name?
</StyledInputDescription> </StyledInputDescription>
<StyledInput <StyledInput
autoFocus autoFocus
label='Incoming webhook name' label='Signal endpoint name'
error={Boolean(errors.name)} error={Boolean(errors.name)}
errorText={errors.name} errorText={errors.name}
value={name} value={name}
@ -127,23 +127,23 @@ export const IncomingWebhooksForm = ({
autoComplete='off' autoComplete='off'
/> />
<StyledInputDescription> <StyledInputDescription>
What is your new incoming webhook description? What is your new signal endpoint description?
</StyledInputDescription> </StyledInputDescription>
<StyledInput <StyledInput
label='Incoming webhook description' label='Signal endpoint description'
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
autoComplete='off' autoComplete='off'
/> />
<IncomingWebhooksFormURL name={name} /> <SignalEndpointsFormURL name={name} />
<ConditionallyRender <ConditionallyRender
condition={incomingWebhook === undefined} condition={signalEndpoint === undefined}
show={ show={
<StyledSecondarySection> <StyledSecondarySection>
<StyledInputDescription>Token</StyledInputDescription> <StyledInputDescription>Token</StyledInputDescription>
<StyledInputSecondaryDescription> <StyledInputSecondaryDescription>
In order to connect your newly created incoming In order to connect your newly created signal
webhook, you will also need a token.{' '} endpoint, you will also need a token.{' '}
<Link <Link
href='https://docs.getunleash.io/reference/api-tokens-and-client-keys' href='https://docs.getunleash.io/reference/api-tokens-and-client-keys'
target='_blank' target='_blank'
@ -184,8 +184,8 @@ export const IncomingWebhooksForm = ({
</FormControl> </FormControl>
<StyledInlineContainer> <StyledInlineContainer>
<StyledInputSecondaryDescription> <StyledInputSecondaryDescription>
A new incoming webhook token will be generated A new signal endpoint token will be generated
for the incoming webhook, so you can get started for the signal endpoint, so you can get started
right away. right away.
</StyledInputSecondaryDescription> </StyledInputSecondaryDescription>
<ConditionallyRender <ConditionallyRender
@ -221,10 +221,10 @@ export const IncomingWebhooksForm = ({
elseShow={ elseShow={
<> <>
<StyledInputDescription> <StyledInputDescription>
Incoming webhook tokens Signal endpoint tokens
</StyledInputDescription> </StyledInputDescription>
<IncomingWebhooksTokens <SignalEndpointsTokens
incomingWebhook={incomingWebhook!} signalEndpoint={signalEndpoint!}
/> />
</> </>
} }

View File

@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
const StyledIncomingWebhookUrlSection = styled('div')(({ theme }) => ({ const StyledSignalEndpointUrlSection = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: theme.spacing(1.5), padding: theme.spacing(1.5),
@ -15,11 +15,11 @@ const StyledIncomingWebhookUrlSection = styled('div')(({ theme }) => ({
marginTop: theme.spacing(3), marginTop: theme.spacing(3),
})); }));
const StyledIncomingWebhookUrlSectionDescription = styled('p')(({ theme }) => ({ const StyledSignalEndpointUrlSectionDescription = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
const StyledIncomingWebhookUrl = styled('div')(({ theme }) => ({ const StyledSignalEndpointUrl = styled('div')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
backgroundColor: theme.palette.background.elevation2, backgroundColor: theme.palette.background.elevation2,
padding: theme.spacing(0.5, 1, 0.5, 2), padding: theme.spacing(0.5, 1, 0.5, 2),
@ -31,17 +31,17 @@ const StyledIncomingWebhookUrl = styled('div')(({ theme }) => ({
wordBreak: 'break-all', wordBreak: 'break-all',
})); }));
interface IIncomingWebhooksFormURLProps { interface ISignalEndpointsFormURLProps {
name: string; name: string;
} }
export const IncomingWebhooksFormURL = ({ export const SignalEndpointsFormURL = ({
name, name,
}: IIncomingWebhooksFormURLProps) => { }: ISignalEndpointsFormURLProps) => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { setToastData } = useToast(); const { setToastData } = useToast();
const url = `${uiConfig.unleashUrl}/api/incoming-webhook/${name}`; const url = `${uiConfig.unleashUrl}/api/signal-endpoint/${name}`;
const onCopyToClipboard = () => { const onCopyToClipboard = () => {
copy(url); copy(url);
@ -52,18 +52,18 @@ export const IncomingWebhooksFormURL = ({
}; };
return ( return (
<StyledIncomingWebhookUrlSection> <StyledSignalEndpointUrlSection>
<StyledIncomingWebhookUrlSectionDescription> <StyledSignalEndpointUrlSectionDescription>
Incoming webhook URL: Signal endpoint URL:
</StyledIncomingWebhookUrlSectionDescription> </StyledSignalEndpointUrlSectionDescription>
<StyledIncomingWebhookUrl> <StyledSignalEndpointUrl>
{url} {url}
<Tooltip title='Copy URL' arrow> <Tooltip title='Copy URL' arrow>
<IconButton onClick={onCopyToClipboard} size='large'> <IconButton onClick={onCopyToClipboard} size='large'>
<CopyIcon /> <CopyIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</StyledIncomingWebhookUrl> </StyledSignalEndpointUrl>
</StyledIncomingWebhookUrlSection> </StyledSignalEndpointUrlSection>
); );
}; };

View File

@ -15,27 +15,24 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { PAT_LIMIT } from '@server/util/constants'; import { PAT_LIMIT } from '@server/util/constants';
import { useIncomingWebhookTokens } from 'hooks/api/getters/useIncomingWebhookTokens/useIncomingWebhookTokens'; import { useSignalEndpointTokens } from 'hooks/api/getters/useSignalEndpointTokens/useSignalEndpointTokens';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table'; import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { IncomingWebhooksTokensCreateDialog } from './IncomingWebhooksTokensCreateDialog'; import { SignalEndpointsTokensCreateDialog } from './SignalEndpointsTokensCreateDialog';
import { IncomingWebhooksTokensDialog } from './IncomingWebhooksTokensDialog'; import { SignalEndpointsTokensDialog } from './SignalEndpointsTokensDialog';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { import {
IncomingWebhookTokenPayload, SignalEndpointTokenPayload,
useIncomingWebhookTokensApi, useSignalEndpointTokensApi,
} from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi'; } from 'hooks/api/actions/useSignalEndpointTokensApi/useSignalEndpointTokensApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { import { ISignalEndpoint, ISignalEndpointToken } from 'interfaces/signal';
IIncomingWebhook, import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
IIncomingWebhookToken,
} from 'interfaces/incomingWebhook';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
const StyledHeader = styled('div')(({ theme }) => ({ const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -74,21 +71,21 @@ export type PageQueryType = Partial<
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true }; const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
interface IIncomingWebhooksTokensProps { interface ISignalEndpointsTokensProps {
incomingWebhook: IIncomingWebhook; signalEndpoint: ISignalEndpoint;
} }
export const IncomingWebhooksTokens = ({ export const SignalEndpointsTokens = ({
incomingWebhook, signalEndpoint,
}: IIncomingWebhooksTokensProps) => { }: ISignalEndpointsTokensProps) => {
const theme = useTheme(); const theme = useTheme();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const { incomingWebhookTokens, refetch: refetchTokens } = const { signalEndpointTokens, refetch: refetchTokens } =
useIncomingWebhookTokens(incomingWebhook.id); useSignalEndpointTokens(signalEndpoint.id);
const { refetch } = useIncomingWebhooks(); const { refetch } = useSignalEndpoints();
const { addIncomingWebhookToken, removeIncomingWebhookToken } = const { addSignalEndpointToken, removeSignalEndpointToken } =
useIncomingWebhookTokensApi(); useSignalEndpointTokensApi();
const [initialState] = useState(() => ({ const [initialState] = useState(() => ({
sortBy: [defaultSort], sortBy: [defaultSort],
@ -99,12 +96,12 @@ export const IncomingWebhooksTokens = ({
const [tokenOpen, setTokenOpen] = useState(false); const [tokenOpen, setTokenOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [newToken, setNewToken] = useState(''); const [newToken, setNewToken] = useState('');
const [selectedToken, setSelectedToken] = useState<IIncomingWebhookToken>(); const [selectedToken, setSelectedToken] = useState<ISignalEndpointToken>();
const onCreateClick = async (newToken: IncomingWebhookTokenPayload) => { const onCreateClick = async (newToken: SignalEndpointTokenPayload) => {
try { try {
const { token } = await addIncomingWebhookToken( const { token } = await addSignalEndpointToken(
incomingWebhook.id, signalEndpoint.id,
newToken, newToken,
); );
refetch(); refetch();
@ -124,8 +121,8 @@ export const IncomingWebhooksTokens = ({
const onDeleteClick = async () => { const onDeleteClick = async () => {
if (selectedToken) { if (selectedToken) {
try { try {
await removeIncomingWebhookToken( await removeSignalEndpointToken(
incomingWebhook.id, signalEndpoint.id,
selectedToken.id, selectedToken.id,
); );
refetch(); refetch();
@ -186,7 +183,7 @@ export const IncomingWebhooksTokens = ({
const { data, getSearchText, getSearchContext } = useSearch( const { data, getSearchText, getSearchContext } = useSearch(
columns, columns,
searchValue, searchValue,
incomingWebhookTokens, signalEndpointTokens,
); );
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable( const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
@ -226,7 +223,7 @@ export const IncomingWebhooksTokens = ({
<Button <Button
variant='contained' variant='contained'
color='primary' color='primary'
disabled={incomingWebhookTokens.length >= PAT_LIMIT} disabled={signalEndpointTokens.length >= PAT_LIMIT}
onClick={() => setCreateOpen(true)} onClick={() => setCreateOpen(true)}
> >
New token New token
@ -254,31 +251,31 @@ export const IncomingWebhooksTokens = ({
elseShow={ elseShow={
<StyledTablePlaceholder> <StyledTablePlaceholder>
<StyledPlaceholderTitle> <StyledPlaceholderTitle>
You have no tokens for this incoming webhook You have no tokens for this signal endpoint
yet. yet.
</StyledPlaceholderTitle> </StyledPlaceholderTitle>
<StyledPlaceholderSubtitle> <StyledPlaceholderSubtitle>
Create a token to start using this incoming Create a token to start using this signal
webhook. endpoint.
</StyledPlaceholderSubtitle> </StyledPlaceholderSubtitle>
<Button <Button
variant='outlined' variant='outlined'
onClick={() => setCreateOpen(true)} onClick={() => setCreateOpen(true)}
> >
Create new incoming webhook token Create new signal endpoint token
</Button> </Button>
</StyledTablePlaceholder> </StyledTablePlaceholder>
} }
/> />
} }
/> />
<IncomingWebhooksTokensCreateDialog <SignalEndpointsTokensCreateDialog
open={createOpen} open={createOpen}
setOpen={setCreateOpen} setOpen={setCreateOpen}
tokens={incomingWebhookTokens} tokens={signalEndpointTokens}
onCreateClick={onCreateClick} onCreateClick={onCreateClick}
/> />
<IncomingWebhooksTokensDialog <SignalEndpointsTokensDialog
open={tokenOpen} open={tokenOpen}
setOpen={setTokenOpen} setOpen={setTokenOpen}
token={newToken} token={newToken}
@ -296,7 +293,7 @@ export const IncomingWebhooksTokens = ({
<Typography> <Typography>
Any applications or scripts using this token " Any applications or scripts using this token "
<strong>{selectedToken?.name}</strong>" will no longer be <strong>{selectedToken?.name}</strong>" will no longer be
able to make requests to this incoming webhook. You cannot able to make requests to this signal endpoint. You cannot
undo this action. undo this action.
</Typography> </Typography>
</Dialogue> </Dialogue>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { IncomingWebhookTokenPayload } from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi'; import { SignalEndpointTokenPayload } from 'hooks/api/actions/useSignalEndpointTokensApi/useSignalEndpointTokensApi';
import { IIncomingWebhookToken } from 'interfaces/incomingWebhook'; import { ISignalEndpointToken } from 'interfaces/signal';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
@ -19,19 +19,19 @@ const StyledInput = styled(Input)(({ theme }) => ({
maxWidth: theme.spacing(50), maxWidth: theme.spacing(50),
})); }));
interface IIncomingWebhooksTokensCreateDialogProps { interface ISignalEndpointsTokensCreateDialogProps {
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
tokens: IIncomingWebhookToken[]; tokens: ISignalEndpointToken[];
onCreateClick: (newToken: IncomingWebhookTokenPayload) => void; onCreateClick: (newToken: SignalEndpointTokenPayload) => void;
} }
export const IncomingWebhooksTokensCreateDialog = ({ export const SignalEndpointsTokensCreateDialog = ({
open, open,
setOpen, setOpen,
tokens, tokens,
onCreateClick, onCreateClick,
}: IIncomingWebhooksTokensCreateDialogProps) => { }: ISignalEndpointsTokensCreateDialogProps) => {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');

View File

@ -6,17 +6,17 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),
})); }));
interface IIncomingWebhooksTokensDialogProps { interface ISignalEndpointsTokensDialogProps {
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
token?: string; token?: string;
} }
export const IncomingWebhooksTokensDialog = ({ export const SignalEndpointsTokensDialog = ({
open, open,
setOpen, setOpen,
token, token,
}: IIncomingWebhooksTokensDialogProps) => ( }: ISignalEndpointsTokensDialogProps) => (
<Dialogue <Dialogue
open={open} open={open}
secondaryButtonText='Close' secondaryButtonText='Close'
@ -25,10 +25,10 @@ export const IncomingWebhooksTokensDialog = ({
setOpen(false); setOpen(false);
} }
}} }}
title='Incoming webhook token created' title='Signal endpoint token created'
> >
<StyledAlert severity='info'> <StyledAlert severity='info'>
Make sure to copy your incoming webhook token now. You won't be able Make sure to copy your signal endpoint token now. You won't be able
to see it again! to see it again!
</StyledAlert> </StyledAlert>
<Typography variant='body1'>Your token:</Typography> <Typography variant='body1'>Your token:</Typography>

View File

@ -1,6 +1,6 @@
import { URL_SAFE_BASIC } from '@server/util/constants'; import { URL_SAFE_BASIC } from '@server/util/constants';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
enum ErrorField { enum ErrorField {
@ -8,20 +8,20 @@ enum ErrorField {
TOKEN_NAME = 'tokenName', TOKEN_NAME = 'tokenName',
} }
const DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS = { const DEFAULT_SIGNAL_ENDPOINTS_FORM_ERRORS = {
[ErrorField.NAME]: undefined, [ErrorField.NAME]: undefined,
[ErrorField.TOKEN_NAME]: undefined, [ErrorField.TOKEN_NAME]: undefined,
}; };
export type IncomingWebhooksFormErrors = Record<ErrorField, string | undefined>; export type SignalEndpointsFormErrors = Record<ErrorField, string | undefined>;
export enum TokenGeneration { export enum TokenGeneration {
LATER = 'later', LATER = 'later',
NOW = 'now', NOW = 'now',
} }
export const useIncomingWebhooksForm = (incomingWebhook?: IIncomingWebhook) => { export const useSignalEndpointsForm = (signalEndpoint?: ISignalEndpoint) => {
const { incomingWebhooks } = useIncomingWebhooks(); const { signalEndpoints } = useSignalEndpoints();
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [name, setName] = useState(''); const [name, setName] = useState('');
@ -32,21 +32,21 @@ export const useIncomingWebhooksForm = (incomingWebhook?: IIncomingWebhook) => {
const [tokenName, setTokenName] = useState(''); const [tokenName, setTokenName] = useState('');
const reloadForm = () => { const reloadForm = () => {
setEnabled(incomingWebhook?.enabled ?? true); setEnabled(signalEndpoint?.enabled ?? true);
setName(incomingWebhook?.name || ''); setName(signalEndpoint?.name || '');
setDescription(incomingWebhook?.description || ''); setDescription(signalEndpoint?.description || '');
setTokenGeneration(TokenGeneration.LATER); setTokenGeneration(TokenGeneration.LATER);
setTokenName(''); setTokenName('');
setValidated(false); setValidated(false);
setErrors(DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS); setErrors(DEFAULT_SIGNAL_ENDPOINTS_FORM_ERRORS);
}; };
useEffect(() => { useEffect(() => {
reloadForm(); reloadForm();
}, [incomingWebhook]); }, [signalEndpoint]);
const [errors, setErrors] = useState<IncomingWebhooksFormErrors>( const [errors, setErrors] = useState<SignalEndpointsFormErrors>(
DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS, DEFAULT_SIGNAL_ENDPOINTS_FORM_ERRORS,
); );
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
@ -61,8 +61,8 @@ export const useIncomingWebhooksForm = (incomingWebhook?: IIncomingWebhook) => {
const isEmpty = (value: string) => !value.length; const isEmpty = (value: string) => !value.length;
const isNameNotUnique = (value: string) => const isNameNotUnique = (value: string) =>
incomingWebhooks?.some( signalEndpoints?.some(
({ id, name }) => id !== incomingWebhook?.id && name === value, ({ id, name }) => id !== signalEndpoint?.id && name === value,
); );
const isNameInvalid = (value: string) => !URL_SAFE_BASIC.test(value); const isNameInvalid = (value: string) => !URL_SAFE_BASIC.test(value);

View File

@ -5,18 +5,18 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { import {
IncomingWebhookPayload, SignalEndpointPayload,
useIncomingWebhooksApi, useSignalEndpointsApi,
} from 'hooks/api/actions/useIncomingWebhooksApi/useIncomingWebhooksApi'; } from 'hooks/api/actions/useSignalEndpointsApi/useSignalEndpointsApi';
import { useIncomingWebhookTokensApi } from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi'; import { useSignalEndpointTokensApi } from 'hooks/api/actions/useSignalEndpointTokensApi/useSignalEndpointTokensApi';
import { IncomingWebhooksForm } from './IncomingWebhooksForm/IncomingWebhooksForm'; import { SignalEndpointsForm } from './SignalEndpointsForm/SignalEndpointsForm';
import { import {
TokenGeneration, TokenGeneration,
useIncomingWebhooksForm, useSignalEndpointsForm,
} from './IncomingWebhooksForm/useIncomingWebhooksForm'; } from './SignalEndpointsForm/useSignalEndpointsForm';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const StyledHeader = styled('div')(({ theme }) => ({ const StyledHeader = styled('div')(({ theme }) => ({
@ -48,25 +48,25 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3), marginLeft: theme.spacing(3),
})); }));
interface IIncomingWebhooksModalProps { interface ISignalEndpointsModalProps {
incomingWebhook?: IIncomingWebhook; signalEndpoint?: ISignalEndpoint;
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
newToken: (token: string) => void; newToken: (token: string) => void;
onOpenEvents: () => void; onOpenSignals: () => void;
} }
export const IncomingWebhooksModal = ({ export const SignalEndpointsModal = ({
incomingWebhook, signalEndpoint,
open, open,
setOpen, setOpen,
newToken, newToken,
onOpenEvents, onOpenSignals,
}: IIncomingWebhooksModalProps) => { }: ISignalEndpointsModalProps) => {
const { refetch } = useIncomingWebhooks(); const { refetch } = useSignalEndpoints();
const { addIncomingWebhook, updateIncomingWebhook, loading } = const { addSignalEndpoint, updateSignalEndpoint, loading } =
useIncomingWebhooksApi(); useSignalEndpointsApi();
const { addIncomingWebhookToken } = useIncomingWebhookTokensApi(); const { addSignalEndpointToken } = useSignalEndpointTokensApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -87,16 +87,16 @@ export const IncomingWebhooksModal = ({
validate, validate,
validated, validated,
reloadForm, reloadForm,
} = useIncomingWebhooksForm(incomingWebhook); } = useSignalEndpointsForm(signalEndpoint);
useEffect(() => { useEffect(() => {
reloadForm(); reloadForm();
}, [open]); }, [open]);
const editing = incomingWebhook !== undefined; const editing = signalEndpoint !== undefined;
const title = `${editing ? 'Edit' : 'New'} incoming webhook`; const title = `${editing ? 'Edit' : 'New'} signal endpoint`;
const payload: IncomingWebhookPayload = { const payload: SignalEndpointPayload = {
enabled, enabled,
name, name,
description, description,
@ -104,8 +104,8 @@ export const IncomingWebhooksModal = ({
const formatApiCode = () => `curl --location --request ${ const formatApiCode = () => `curl --location --request ${
editing ? 'PUT' : 'POST' editing ? 'PUT' : 'POST'
} '${uiConfig.unleashUrl}/api/admin/incoming-webhooks${ } '${uiConfig.unleashUrl}/api/admin/signal-endpoints${
editing ? `/${incomingWebhook.id}` : '' editing ? `/${signalEndpoint.id}` : ''
}' \\ }' \\
--header 'Authorization: INSERT_API_KEY' \\ --header 'Authorization: INSERT_API_KEY' \\
--header 'Content-Type: application/json' \\ --header 'Content-Type: application/json' \\
@ -118,18 +118,18 @@ export const IncomingWebhooksModal = ({
try { try {
if (editing) { if (editing) {
await updateIncomingWebhook(incomingWebhook.id, payload); await updateSignalEndpoint(signalEndpoint.id, payload);
} else { } else {
const { id } = await addIncomingWebhook(payload); const { id } = await addSignalEndpoint(payload);
if (tokenGeneration === TokenGeneration.NOW) { if (tokenGeneration === TokenGeneration.NOW) {
const { token } = await addIncomingWebhookToken(id, { const { token } = await addSignalEndpointToken(id, {
name: tokenName, name: tokenName,
}); });
newToken(token); newToken(token);
} }
} }
setToastData({ setToastData({
title: `Incoming webhook ${ title: `Signal endpoint ${
editing ? 'updated' : 'added' editing ? 'updated' : 'added'
} successfully`, } successfully`,
type: 'success', type: 'success',
@ -152,21 +152,21 @@ export const IncomingWebhooksModal = ({
<FormTemplate <FormTemplate
loading={loading} loading={loading}
modal modal
description='Incoming Webhooks allow third-party services to send observable events to Unleash.' description='Signal endpoints allow third-party services to send signals to Unleash.'
documentationLink='https://docs.getunleash.io/reference/incoming-webhooks' documentationLink='https://docs.getunleash.io/reference/signals'
documentationLinkLabel='Incoming webhooks documentation' documentationLinkLabel='Signals documentation'
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
<StyledHeader> <StyledHeader>
<StyledTitle>{title}</StyledTitle> <StyledTitle>{title}</StyledTitle>
<ConditionallyRender <ConditionallyRender
condition={editing} condition={editing}
show={<Link onClick={onOpenEvents}>View events</Link>} show={<Link onClick={onOpenSignals}>View signals</Link>}
/> />
</StyledHeader> </StyledHeader>
<StyledForm onSubmit={onSubmit}> <StyledForm onSubmit={onSubmit}>
<IncomingWebhooksForm <SignalEndpointsForm
incomingWebhook={incomingWebhook} signalEndpoint={signalEndpoint}
enabled={enabled} enabled={enabled}
setEnabled={setEnabled} setEnabled={setEnabled}
name={name} name={name}
@ -188,7 +188,7 @@ export const IncomingWebhooksModal = ({
variant='contained' variant='contained'
color='primary' color='primary'
> >
{editing ? 'Save' : 'Add'} incoming webhook {editing ? 'Save' : 'Add'} signal endpoint
</Button> </Button>
<StyledCancelButton <StyledCancelButton
onClick={() => { onClick={() => {

View File

@ -1,7 +1,7 @@
import { Button, Link, 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 { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { useIncomingWebhookEvents } from 'hooks/api/getters/useIncomingWebhookEvents/useIncomingWebhookEvents'; import { useSignalEndpointSignals } from 'hooks/api/getters/useSignalEndpointSignals/useSignalEndpointSignals';
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import FormTemplate from 'component/common/FormTemplate/FormTemplate'; import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
@ -55,31 +55,31 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({
paddingTop: theme.spacing(4), paddingTop: theme.spacing(4),
})); }));
interface IIncomingWebhooksEventsModalProps { interface ISignalEndpointsSignalsModalProps {
incomingWebhook?: IIncomingWebhook; signalEndpoint?: ISignalEndpoint;
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onOpenConfiguration: () => void; onOpenConfiguration: () => void;
} }
export const IncomingWebhooksEventsModal = ({ export const SignalEndpointsSignalsModal = ({
incomingWebhook, signalEndpoint,
open, open,
setOpen, setOpen,
onOpenConfiguration, onOpenConfiguration,
}: IIncomingWebhooksEventsModalProps) => { }: ISignalEndpointsSignalsModalProps) => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
const { incomingWebhookEvents, hasMore, loadMore, loading } = const { signalEndpointSignals, hasMore, loadMore, loading } =
useIncomingWebhookEvents(incomingWebhook?.id, 20, { useSignalEndpointSignals(signalEndpoint?.id, 20, {
refreshInterval: 5000, refreshInterval: 5000,
}); });
if (!incomingWebhook) { if (!signalEndpoint) {
return null; return null;
} }
const title = `Events: ${incomingWebhook.name}`; const title = `Signals: ${signalEndpoint.name}`;
return ( return (
<SidebarModal <SidebarModal
@ -90,11 +90,11 @@ export const IncomingWebhooksEventsModal = ({
label={title} label={title}
> >
<FormTemplate <FormTemplate
loading={loading && incomingWebhookEvents.length === 0} loading={loading && signalEndpointSignals.length === 0}
modal modal
description='Incoming Webhooks allow third-party services to send observable events to Unleash.' description=''
documentationLink='https://docs.getunleash.io/reference/incoming-webhooks' documentationLink=''
documentationLinkLabel='Incoming webhooks documentation' documentationLinkLabel=''
showGuidance={false} showGuidance={false}
> >
<StyledHeader> <StyledHeader>
@ -106,32 +106,32 @@ export const IncomingWebhooksEventsModal = ({
</StyledHeaderRow> </StyledHeaderRow>
<StyledHeaderSubtitle> <StyledHeaderSubtitle>
<p> <p>
{uiConfig.unleashUrl}/api/incoming-webhook/ {uiConfig.unleashUrl}/api/signal-endpoint/
{incomingWebhook.name} {signalEndpoint.name}
</p> </p>
<StyledDescription> <StyledDescription>
{incomingWebhook.description} {signalEndpoint.description}
</StyledDescription> </StyledDescription>
</StyledHeaderSubtitle> </StyledHeaderSubtitle>
</StyledHeader> </StyledHeader>
<StyledForm> <StyledForm>
<SidePanelList <SidePanelList
height={960} height={960}
items={incomingWebhookEvents} items={signalEndpointSignals}
columns={[ columns={[
{ {
header: 'Date', header: 'Date',
maxWidth: 180, maxWidth: 180,
cell: (event) => cell: ({ createdAt }) =>
formatDateYMDHMS( formatDateYMDHMS(
event.createdAt, createdAt,
locationSettings?.locale, locationSettings?.locale,
), ),
}, },
{ {
header: 'Token', header: 'Token',
maxWidth: 350, maxWidth: 350,
cell: (event) => event.tokenName, cell: ({ tokenName }) => tokenName,
}, },
]} ]}
sidePanelHeader='Payload' sidePanelHeader='Payload'
@ -157,11 +157,11 @@ export const IncomingWebhooksEventsModal = ({
} }
/> />
<ConditionallyRender <ConditionallyRender
condition={incomingWebhookEvents.length === 0} condition={signalEndpointSignals.length === 0}
show={ show={
<p> <p>
No events have been received for this incoming No signals have been received on this signal
webhook. endpoint.
</p> </p>
} }
/> />

View File

@ -23,21 +23,21 @@ const StyledBoxCell = styled(Box)({
justifyContent: 'center', justifyContent: 'center',
}); });
interface IIncomingWebhooksActionsCellProps { interface ISignalEndpointsActionsCellProps {
incomingWebhookId: number; signalEndpointId: number;
onCopyToClipboard: (event: React.SyntheticEvent) => void; onCopyToClipboard: (event: React.SyntheticEvent) => void;
onOpenEvents: (event: React.SyntheticEvent) => void; onOpenSignals: (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 IncomingWebhooksActionsCell = ({ export const SignalEndpointsActionsCell = ({
incomingWebhookId, signalEndpointId,
onCopyToClipboard, onCopyToClipboard,
onOpenEvents, onOpenSignals,
onEdit, onEdit,
onDelete, onDelete,
}: IIncomingWebhooksActionsCellProps) => { }: ISignalEndpointsActionsCellProps) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
@ -50,12 +50,12 @@ export const IncomingWebhooksActionsCell = ({
setAnchorEl(null); setAnchorEl(null);
}; };
const id = `incoming-webhook-${incomingWebhookId}-actions`; const id = `signal-endpoint-${signalEndpointId}-actions`;
const menuId = `${id}-menu`; const menuId = `${id}-menu`;
return ( return (
<StyledBoxCell> <StyledBoxCell>
<Tooltip title='Incoming webhook actions' arrow describeChild> <Tooltip title='Signal endpoint actions' arrow describeChild>
<IconButton <IconButton
id={id} id={id}
data-loading data-loading
@ -100,7 +100,7 @@ export const IncomingWebhooksActionsCell = ({
{({ hasAccess }) => ( {({ hasAccess }) => (
<MenuItem <MenuItem
sx={defaultBorderRadius} sx={defaultBorderRadius}
onClick={onOpenEvents} onClick={onOpenSignals}
disabled={!hasAccess} disabled={!hasAccess}
> >
<ListItemIcon> <ListItemIcon>
@ -108,7 +108,7 @@ export const IncomingWebhooksActionsCell = ({
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
<Typography variant='body2'> <Typography variant='body2'>
View events View signals
</Typography> </Typography>
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>

View File

@ -0,0 +1,32 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { ISignalEndpoint } from 'interfaces/signal';
interface ISignalEndpointsDeleteDialogProps {
signalEndpoint?: ISignalEndpoint;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
onConfirm: (signalEndpoint: ISignalEndpoint) => void;
}
export const SignalEndpointsDeleteDialog = ({
signalEndpoint,
open,
setOpen,
onConfirm,
}: ISignalEndpointsDeleteDialogProps) => (
<Dialogue
title='Delete signal endpoint?'
open={open}
primaryButtonText='Delete signal endpoint'
secondaryButtonText='Cancel'
onClick={() => onConfirm(signalEndpoint!)}
onClose={() => {
setOpen(false);
}}
>
<p>
You are about to delete signal endpoint:{' '}
<strong>{signalEndpoint?.name}</strong>
</p>
</Dialogue>
);

View File

@ -3,65 +3,55 @@ import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useMediaQuery } from '@mui/material'; import { Button, useMediaQuery } from '@mui/material';
import { useFlexLayout, useSortBy, useTable } from 'react-table'; import { useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks'; import { useSignalEndpoints } from 'hooks/api/getters/useSignalEndpoints/useSignalEndpoints';
import { useIncomingWebhooksApi } from 'hooks/api/actions/useIncomingWebhooksApi/useIncomingWebhooksApi'; import { useSignalEndpointsApi } from 'hooks/api/actions/useSignalEndpointsApi/useSignalEndpointsApi';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { IncomingWebhooksActionsCell } from './IncomingWebhooksActionsCell'; import { SignalEndpointsActionsCell } from './SignalEndpointsActionsCell';
import { IncomingWebhooksDeleteDialog } from './IncomingWebhooksDeleteDialog'; import { SignalEndpointsDeleteDialog } from './SignalEndpointsDeleteDialog';
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell'; import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { IncomingWebhookTokensCell } from './IncomingWebhooksTokensCell'; import { SignalEndpointsTokensCell } from './SignalEndpointsTokensCell';
import { IncomingWebhooksModal } from '../IncomingWebhooksModal/IncomingWebhooksModal'; import { SignalEndpointsModal } from '../SignalEndpointsModal/SignalEndpointsModal';
import { IncomingWebhooksTokensDialog } from '../IncomingWebhooksModal/IncomingWebhooksForm/IncomingWebhooksTokens/IncomingWebhooksTokensDialog'; import { SignalEndpointsTokensDialog } from '../SignalEndpointsModal/SignalEndpointsForm/SignalEndpointsTokens/SignalEndpointsTokensDialog';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { IncomingWebhooksEventsModal } from '../IncomingWebhooksEvents/IncomingWebhooksEventsModal'; import { SignalEndpointsSignalsModal } from '../SignalEndpointsSignals/SignalEndpointsSignalsModal';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
interface IIncomingWebhooksTableProps { export const SignalEndpointsTable = () => {
modalOpen: boolean;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
selectedIncomingWebhook?: IIncomingWebhook;
setSelectedIncomingWebhook: React.Dispatch<
React.SetStateAction<IIncomingWebhook | undefined>
>;
}
export const IncomingWebhooksTable = ({
modalOpen,
setModalOpen,
selectedIncomingWebhook,
setSelectedIncomingWebhook,
}: IIncomingWebhooksTableProps) => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { incomingWebhooks, refetch } = useIncomingWebhooks(); const { signalEndpoints, refetch } = useSignalEndpoints();
const { toggleIncomingWebhook, removeIncomingWebhook } = const { toggleSignalEndpoint, removeSignalEndpoint } =
useIncomingWebhooksApi(); useSignalEndpointsApi();
const [selectedSignalEndpoint, setSelectedSignalEndpoint] =
useState<ISignalEndpoint>();
const [modalOpen, setModalOpen] = useState(false);
const [tokenDialog, setTokenDialog] = useState(false); const [tokenDialog, setTokenDialog] = useState(false);
const [newToken, setNewToken] = useState(''); const [newToken, setNewToken] = useState('');
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [eventsModalOpen, setEventsModalOpen] = useState(false); const [signalsModalOpen, setSignalsModalOpen] = useState(false);
const onToggleIncomingWebhook = async ( const onToggleSignalEndpoint = async (
incomingWebhook: IIncomingWebhook, { id, name }: ISignalEndpoint,
enabled: boolean, enabled: boolean,
) => { ) => {
try { try {
await toggleIncomingWebhook(incomingWebhook.id, enabled); await toggleSignalEndpoint(id, enabled);
setToastData({ setToastData({
title: `"${incomingWebhook.name}" has been ${ title: `"${name}" has been ${enabled ? 'enabled' : 'disabled'}`,
enabled ? 'enabled' : 'disabled'
}`,
type: 'success', type: 'success',
}); });
refetch(); refetch();
@ -70,11 +60,11 @@ export const IncomingWebhooksTable = ({
} }
}; };
const onDeleteConfirm = async (incomingWebhook: IIncomingWebhook) => { const onDeleteConfirm = async ({ id, name }: ISignalEndpoint) => {
try { try {
await removeIncomingWebhook(incomingWebhook.id); await removeSignalEndpoint(id);
setToastData({ setToastData({
title: `"${incomingWebhook.name}" has been deleted`, title: `"${name}" has been deleted`,
type: 'success', type: 'success',
}); });
refetch(); refetch();
@ -92,42 +82,42 @@ export const IncomingWebhooksTable = ({
Header: 'Name', Header: 'Name',
accessor: 'name', accessor: 'name',
Cell: ({ Cell: ({
row: { original: incomingWebhook }, row: { original: signalEndpoint },
}: { row: { original: IIncomingWebhook } }) => ( }: { row: { original: ISignalEndpoint } }) => (
<LinkCell <LinkCell
title={incomingWebhook.name} title={signalEndpoint.name}
onClick={() => { onClick={() => {
setSelectedIncomingWebhook(incomingWebhook); setSelectedSignalEndpoint(signalEndpoint);
setModalOpen(true); setModalOpen(true);
}} }}
subtitle={incomingWebhook.description} subtitle={signalEndpoint.description}
/> />
), ),
width: 240, width: 240,
}, },
{ {
Header: 'URL', Header: 'URL',
accessor: (row: IIncomingWebhook) => accessor: (row: ISignalEndpoint) =>
`${uiConfig.unleashUrl}/api/incoming-webhook/${row.name}`, `${uiConfig.unleashUrl}/api/signal-endpoint/${row.name}`,
minWidth: 200, minWidth: 200,
}, },
{ {
id: 'tokens', id: 'tokens',
Header: 'Tokens', Header: 'Tokens',
accessor: (row: IIncomingWebhook) => accessor: (row: ISignalEndpoint) =>
row.tokens?.map(({ name }) => name).join('\n') || '', row.tokens?.map(({ name }) => name).join('\n') || '',
Cell: ({ Cell: ({
row: { original: incomingWebhook }, row: { original: signalEndpoint },
value, value,
}: { }: {
row: { original: IIncomingWebhook }; row: { original: ISignalEndpoint };
value: string; value: string;
}) => ( }) => (
<IncomingWebhookTokensCell <SignalEndpointsTokensCell
incomingWebhook={incomingWebhook} signalEndpoint={signalEndpoint}
value={value} value={value}
onCreateToken={() => { onCreateToken={() => {
setSelectedIncomingWebhook(incomingWebhook); setSelectedSignalEndpoint(signalEndpoint);
setModalOpen(true); setModalOpen(true);
}} }}
/> />
@ -146,12 +136,12 @@ export const IncomingWebhooksTable = ({
Header: 'Enabled', Header: 'Enabled',
accessor: 'enabled', accessor: 'enabled',
Cell: ({ Cell: ({
row: { original: incomingWebhook }, row: { original: signalEndpoint },
}: { row: { original: IIncomingWebhook } }) => ( }: { row: { original: ISignalEndpoint } }) => (
<ToggleCell <ToggleCell
checked={incomingWebhook.enabled} checked={signalEndpoint.enabled}
setChecked={(enabled) => setChecked={(enabled) =>
onToggleIncomingWebhook(incomingWebhook, enabled) onToggleSignalEndpoint(signalEndpoint, enabled)
} }
/> />
), ),
@ -164,29 +154,29 @@ export const IncomingWebhooksTable = ({
id: 'Actions', id: 'Actions',
align: 'center', align: 'center',
Cell: ({ Cell: ({
row: { original: incomingWebhook }, row: { original: signalEndpoint },
}: { row: { original: IIncomingWebhook } }) => ( }: { row: { original: ISignalEndpoint } }) => (
<IncomingWebhooksActionsCell <SignalEndpointsActionsCell
incomingWebhookId={incomingWebhook.id} signalEndpointId={signalEndpoint.id}
onCopyToClipboard={() => { onCopyToClipboard={() => {
copy( copy(
`${uiConfig.unleashUrl}/api/incoming-webhook/${incomingWebhook.name}`, `${uiConfig.unleashUrl}/api/signal-endpoint/${signalEndpoint.name}`,
); );
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Copied to clipboard', title: 'Copied to clipboard',
}); });
}} }}
onOpenEvents={() => { onOpenSignals={() => {
setSelectedIncomingWebhook(incomingWebhook); setSelectedSignalEndpoint(signalEndpoint);
setEventsModalOpen(true); setSignalsModalOpen(true);
}} }}
onEdit={() => { onEdit={() => {
setSelectedIncomingWebhook(incomingWebhook); setSelectedSignalEndpoint(signalEndpoint);
setModalOpen(true); setModalOpen(true);
}} }}
onDelete={() => { onDelete={() => {
setSelectedIncomingWebhook(incomingWebhook); setSelectedSignalEndpoint(signalEndpoint);
setDeleteOpen(true); setDeleteOpen(true);
}} }}
/> />
@ -205,7 +195,7 @@ export const IncomingWebhooksTable = ({
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable( const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
{ {
columns: columns as any, columns: columns as any,
data: incomingWebhooks, data: signalEndpoints,
initialState, initialState,
sortTypes, sortTypes,
autoResetHiddenColumns: false, autoResetHiddenColumns: false,
@ -232,7 +222,25 @@ export const IncomingWebhooksTable = ({
); );
return ( return (
<> <PageContent
header={
<PageHeader
title={`Signal endpoints (${signalEndpoints.length})`}
actions={
<Button
variant='contained'
color='primary'
onClick={() => {
setSelectedSignalEndpoint(undefined);
setModalOpen(true);
}}
>
New signal endpoint
</Button>
}
/>
}
>
<VirtualizedTable <VirtualizedTable
rows={rows} rows={rows}
headerGroups={headerGroups} headerGroups={headerGroups}
@ -242,44 +250,44 @@ export const IncomingWebhooksTable = ({
condition={rows.length === 0} condition={rows.length === 0}
show={ show={
<TablePlaceholder> <TablePlaceholder>
No incoming webhooks available. Get started by adding No signal endpoints available. Get started by adding
one. one.
</TablePlaceholder> </TablePlaceholder>
} }
/> />
<IncomingWebhooksModal <SignalEndpointsModal
incomingWebhook={selectedIncomingWebhook} signalEndpoint={selectedSignalEndpoint}
open={modalOpen} open={modalOpen}
setOpen={setModalOpen} setOpen={setModalOpen}
newToken={(token: string) => { newToken={(token: string) => {
setNewToken(token); setNewToken(token);
setTokenDialog(true); setTokenDialog(true);
}} }}
onOpenEvents={() => { onOpenSignals={() => {
setModalOpen(false); setModalOpen(false);
setEventsModalOpen(true); setSignalsModalOpen(true);
}} }}
/> />
<IncomingWebhooksEventsModal <SignalEndpointsSignalsModal
incomingWebhook={selectedIncomingWebhook} signalEndpoint={selectedSignalEndpoint}
open={eventsModalOpen} open={signalsModalOpen}
setOpen={setEventsModalOpen} setOpen={setSignalsModalOpen}
onOpenConfiguration={() => { onOpenConfiguration={() => {
setEventsModalOpen(false); setSignalsModalOpen(false);
setModalOpen(true); setModalOpen(true);
}} }}
/> />
<IncomingWebhooksTokensDialog <SignalEndpointsTokensDialog
open={tokenDialog} open={tokenDialog}
setOpen={setTokenDialog} setOpen={setTokenDialog}
token={newToken} token={newToken}
/> />
<IncomingWebhooksDeleteDialog <SignalEndpointsDeleteDialog
incomingWebhook={selectedIncomingWebhook} signalEndpoint={selectedSignalEndpoint}
open={deleteOpen} open={deleteOpen}
setOpen={setDeleteOpen} setOpen={setDeleteOpen}
onConfirm={onDeleteConfirm} onConfirm={onDeleteConfirm}
/> />
</> </PageContent>
); );
}; };

View File

@ -2,7 +2,7 @@ import { styled, Typography } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Highlighter } from 'component/common/Highlighter/Highlighter'; import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
@ -10,20 +10,20 @@ const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
})); }));
interface IIncomingWebhookTokensCellProps { interface ISignalEndpointsTokensCellProps {
incomingWebhook: IIncomingWebhook; signalEndpoint: ISignalEndpoint;
value: string; value: string;
onCreateToken?: () => void; onCreateToken?: () => void;
} }
export const IncomingWebhookTokensCell = ({ export const SignalEndpointsTokensCell = ({
incomingWebhook, signalEndpoint: { tokens },
value, value,
onCreateToken, onCreateToken,
}: IIncomingWebhookTokensCellProps) => { }: ISignalEndpointsTokensCellProps) => {
const { searchQuery } = useSearchHighlightContext(); const { searchQuery } = useSearchHighlightContext();
if (!incomingWebhook.tokens || incomingWebhook.tokens.length === 0) { if (!tokens || tokens.length === 0) {
if (!onCreateToken) return <TextCell>0 tokens</TextCell>; if (!onCreateToken) return <TextCell>0 tokens</TextCell>;
else return <LinkCell title='Create token' onClick={onCreateToken} />; else return <LinkCell title='Create token' onClick={onCreateToken} />;
} }
@ -33,7 +33,7 @@ export const IncomingWebhookTokensCell = ({
<TooltipLink <TooltipLink
tooltip={ tooltip={
<> <>
{incomingWebhook.tokens?.map(({ id, name }) => ( {tokens?.map(({ id, name }) => (
<StyledItem key={id}> <StyledItem key={id}>
<Highlighter search={searchQuery}> <Highlighter search={searchQuery}>
{name} {name}
@ -47,9 +47,7 @@ export const IncomingWebhookTokensCell = ({
value.toLowerCase().includes(searchQuery.toLowerCase()) value.toLowerCase().includes(searchQuery.toLowerCase())
} }
> >
{incomingWebhook.tokens?.length === 1 {tokens?.length === 1 ? '1 token' : `${tokens?.length} tokens`}
? '1 token'
: `${incomingWebhook.tokens?.length} tokens`}
</TooltipLink> </TooltipLink>
</TextCell> </TextCell>
); );

View File

@ -0,0 +1,21 @@
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { SignalEndpointsTable } from './SignalEndpointsTable/SignalEndpointsTable';
export const Signals = () => {
const { isEnterprise } = useUiConfig();
if (!isEnterprise()) {
return <PremiumFeature feature='signals' />;
}
return (
<div>
<PermissionGuard permissions={ADMIN}>
<SignalEndpointsTable />
</PermissionGuard>
</div>
);
};

View File

@ -1,77 +0,0 @@
import { IIncomingWebhookToken } from 'interfaces/incomingWebhook';
import useAPI from '../useApi/useApi';
const ENDPOINT = 'api/admin/incoming-webhooks';
export type IncomingWebhookTokenPayload = Omit<
IIncomingWebhookToken,
'id' | 'incomingWebhookId' | 'createdAt' | 'createdByUserId'
>;
type IncomingWebhookTokenWithTokenSecret = IIncomingWebhookToken & {
token: string;
};
export const useIncomingWebhookTokensApi = () => {
const { loading, makeRequest, createRequest, errors } = useAPI({
propagateErrors: true,
});
const addIncomingWebhookToken = async (
incomingWebhookId: number,
incomingWebhookToken: IncomingWebhookTokenPayload,
): Promise<IncomingWebhookTokenWithTokenSecret> => {
const requestId = 'addIncomingWebhookToken';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}/tokens`,
{
method: 'POST',
body: JSON.stringify(incomingWebhookToken),
},
requestId,
);
const response = await makeRequest(req.caller, req.id);
return response.json();
};
const updateIncomingWebhookToken = async (
incomingWebhookId: number,
incomingWebhookTokenId: number,
incomingWebhookToken: IncomingWebhookTokenPayload,
) => {
const requestId = 'updateIncomingWebhookToken';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}/tokens/${incomingWebhookTokenId}`,
{
method: 'PUT',
body: JSON.stringify(incomingWebhookToken),
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const removeIncomingWebhookToken = async (
incomingWebhookId: number,
incomingWebhookTokenId: number,
) => {
const requestId = 'removeIncomingWebhookToken';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}/tokens/${incomingWebhookTokenId}`,
{ method: 'DELETE' },
requestId,
);
await makeRequest(req.caller, req.id);
};
return {
addIncomingWebhookToken,
updateIncomingWebhookToken,
removeIncomingWebhookToken,
errors,
loading,
};
};

View File

@ -1,106 +0,0 @@
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
import useAPI from '../useApi/useApi';
const ENDPOINT = 'api/admin/incoming-webhooks';
export type IncomingWebhookPayload = Omit<
IIncomingWebhook,
'id' | 'createdAt' | 'createdByUserId' | 'tokens'
>;
export const useIncomingWebhooksApi = () => {
const { loading, makeRequest, createRequest, errors } = useAPI({
propagateErrors: true,
});
const addIncomingWebhook = async (
incomingWebhook: IncomingWebhookPayload,
) => {
const requestId = 'addIncomingWebhook';
const req = createRequest(
ENDPOINT,
{
method: 'POST',
body: JSON.stringify(incomingWebhook),
},
requestId,
);
const response = await makeRequest(req.caller, req.id);
return response.json();
};
const updateIncomingWebhook = async (
incomingWebhookId: number,
incomingWebhook: IncomingWebhookPayload,
) => {
const requestId = 'updateIncomingWebhook';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}`,
{
method: 'PUT',
body: JSON.stringify(incomingWebhook),
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const enableIncomingWebhook = async (incomingWebhookId: number) => {
const requestId = 'enableIncomingWebhook';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}/on`,
{
method: 'POST',
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const disableIncomingWebhook = async (incomingWebhookId: number) => {
const requestId = 'disableIncomingWebhook';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}/off`,
{
method: 'POST',
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const toggleIncomingWebhook = async (
incomingWebhookId: number,
enabled: boolean,
) => {
if (enabled) {
await enableIncomingWebhook(incomingWebhookId);
} else {
await disableIncomingWebhook(incomingWebhookId);
}
};
const removeIncomingWebhook = async (incomingWebhookId: number) => {
const requestId = 'removeIncomingWebhook';
const req = createRequest(
`${ENDPOINT}/${incomingWebhookId}`,
{ method: 'DELETE' },
requestId,
);
await makeRequest(req.caller, req.id);
};
return {
addIncomingWebhook,
updateIncomingWebhook,
removeIncomingWebhook,
toggleIncomingWebhook,
errors,
loading,
};
};

View File

@ -0,0 +1,77 @@
import { ISignalEndpointToken } from 'interfaces/signal';
import useAPI from '../useApi/useApi';
const ENDPOINT = 'api/admin/signal-endpoints';
export type SignalEndpointTokenPayload = Omit<
ISignalEndpointToken,
'id' | 'signalEndpointId' | 'createdAt' | 'createdByUserId'
>;
type SignalEndpointTokenWithTokenSecret = ISignalEndpointToken & {
token: string;
};
export const useSignalEndpointTokensApi = () => {
const { loading, makeRequest, createRequest, errors } = useAPI({
propagateErrors: true,
});
const addSignalEndpointToken = async (
signalEndpointId: number,
signalEndpointToken: SignalEndpointTokenPayload,
): Promise<SignalEndpointTokenWithTokenSecret> => {
const requestId = 'addSignalEndpointToken';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}/tokens`,
{
method: 'POST',
body: JSON.stringify(signalEndpointToken),
},
requestId,
);
const response = await makeRequest(req.caller, req.id);
return response.json();
};
const updateSignalEndpointToken = async (
signalEndpointId: number,
signalEndpointTokenId: number,
signalEndpointToken: SignalEndpointTokenPayload,
) => {
const requestId = 'updateSignalEndpointToken';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}/tokens/${signalEndpointTokenId}`,
{
method: 'PUT',
body: JSON.stringify(signalEndpointToken),
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const removeSignalEndpointToken = async (
signalEndpointId: number,
signalEndpointTokenId: number,
) => {
const requestId = 'removeSignalEndpointToken';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}/tokens/${signalEndpointTokenId}`,
{ method: 'DELETE' },
requestId,
);
await makeRequest(req.caller, req.id);
};
return {
addSignalEndpointToken,
updateSignalEndpointToken,
removeSignalEndpointToken,
errors,
loading,
};
};

View File

@ -0,0 +1,104 @@
import { ISignalEndpoint } from 'interfaces/signal';
import useAPI from '../useApi/useApi';
const ENDPOINT = 'api/admin/signal-endpoints';
export type SignalEndpointPayload = Omit<
ISignalEndpoint,
'id' | 'createdAt' | 'createdByUserId' | 'tokens'
>;
export const useSignalEndpointsApi = () => {
const { loading, makeRequest, createRequest, errors } = useAPI({
propagateErrors: true,
});
const addSignalEndpoint = async (signalEndpoint: SignalEndpointPayload) => {
const requestId = 'addSignalEndpoint';
const req = createRequest(
ENDPOINT,
{
method: 'POST',
body: JSON.stringify(signalEndpoint),
},
requestId,
);
const response = await makeRequest(req.caller, req.id);
return response.json();
};
const updateSignalEndpoint = async (
signalEndpointId: number,
signalEndpoint: SignalEndpointPayload,
) => {
const requestId = 'updateSignalEndpoint';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}`,
{
method: 'PUT',
body: JSON.stringify(signalEndpoint),
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const enableSignalEndpoint = async (signalEndpointId: number) => {
const requestId = 'enableSignalEndpoint';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}/on`,
{
method: 'POST',
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const disableSignalEndpoint = async (signalEndpointId: number) => {
const requestId = 'disableSignalEndpoint';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}/off`,
{
method: 'POST',
},
requestId,
);
await makeRequest(req.caller, req.id);
};
const toggleSignalEndpoint = async (
signalEndpointId: number,
enabled: boolean,
) => {
if (enabled) {
await enableSignalEndpoint(signalEndpointId);
} else {
await disableSignalEndpoint(signalEndpointId);
}
};
const removeSignalEndpoint = async (signalEndpointId: number) => {
const requestId = 'removeSignalEndpoint';
const req = createRequest(
`${ENDPOINT}/${signalEndpointId}`,
{ method: 'DELETE' },
requestId,
);
await makeRequest(req.caller, req.id);
};
return {
addSignalEndpoint,
updateSignalEndpoint,
removeSignalEndpoint,
toggleSignalEndpoint,
errors,
loading,
};
};

View File

@ -5,63 +5,62 @@ import useSWRInfinite, {
import { formatApiPath } from 'utils/formatPath'; import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
import { IIncomingWebhookEvent } from 'interfaces/incomingWebhook'; import { ISignalEndpointSignal } from 'interfaces/signal';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
const ENDPOINT = 'api/admin/incoming-webhooks'; const ENDPOINT = 'api/admin/signal-endpoints';
type IncomingWebhookEventsResponse = { type SignalsResponse = {
incomingWebhookEvents: IIncomingWebhookEvent[]; signalEndpointSignals: ISignalEndpointSignal[];
}; };
const fetcher = async (url: string) => { const fetcher = async (url: string) => {
const response = await fetch(url); const response = await fetch(url);
await handleErrorResponses('Incoming webhook events')(response); await handleErrorResponses('Signals')(response);
return response.json(); return response.json();
}; };
export const useIncomingWebhookEvents = ( export const useSignalEndpointSignals = (
incomingWebhookId?: number, signalEndpointId?: number,
limit = 50, limit = 50,
options: SWRInfiniteConfiguration = {}, options: SWRInfiniteConfiguration = {},
) => { ) => {
const { isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks'); const signalsEnabled = useUiFlag('signals');
const getKey: SWRInfiniteKeyLoader = ( const getKey: SWRInfiniteKeyLoader = (
pageIndex: number, pageIndex: number,
previousPageData: IncomingWebhookEventsResponse, previousPageData: SignalsResponse,
) => { ) => {
// Does not meet conditions // Does not meet conditions
if (!incomingWebhookId || !isEnterprise || !incomingWebhooksEnabled) if (!signalEndpointId || !isEnterprise || !signalsEnabled) return null;
return null;
// Reached the end // Reached the end
if (previousPageData && !previousPageData.incomingWebhookEvents.length) if (previousPageData && !previousPageData.signalEndpointSignals.length)
return null; return null;
return formatApiPath( return formatApiPath(
`${ENDPOINT}/${incomingWebhookId}/events?limit=${limit}&offset=${ `${ENDPOINT}/${signalEndpointId}/signals?limit=${limit}&offset=${
pageIndex * limit pageIndex * limit
}`, }`,
); );
}; };
const { data, error, size, setSize, mutate } = const { data, error, size, setSize, mutate } =
useSWRInfinite<IncomingWebhookEventsResponse>(getKey, fetcher, { useSWRInfinite<SignalsResponse>(getKey, fetcher, {
...options, ...options,
revalidateAll: true, revalidateAll: true,
}); });
const incomingWebhookEvents = data const signalEndpointSignals = data
? data.flatMap(({ incomingWebhookEvents }) => incomingWebhookEvents) ? data.flatMap(({ signalEndpointSignals }) => signalEndpointSignals)
: []; : [];
const isLoadingInitialData = !data && !error; const isLoadingInitialData = !data && !error;
const isLoadingMore = size > 0 && !data?.[size - 1]; const isLoadingMore = size > 0 && !data?.[size - 1];
const loading = isLoadingInitialData || isLoadingMore; const loading = isLoadingInitialData || isLoadingMore;
const hasMore = data?.[size - 1]?.incomingWebhookEvents.length === limit; const hasMore = data?.[size - 1]?.signalEndpointSignals.length === limit;
const loadMore = () => { const loadMore = () => {
if (loading || !hasMore) return; if (loading || !hasMore) return;
@ -69,7 +68,7 @@ export const useIncomingWebhookEvents = (
}; };
return { return {
incomingWebhookEvents, signalEndpointSignals,
hasMore, hasMore,
loadMore, loadMore,
loading, loading,

View File

@ -3,26 +3,31 @@ import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
import { IIncomingWebhookToken } from 'interfaces/incomingWebhook'; import { ISignalEndpointToken } from 'interfaces/signal';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
const ENDPOINT = 'api/admin/incoming-webhooks'; const ENDPOINT = 'api/admin/signal-endpoints';
export const useIncomingWebhookTokens = (incomingWebhookId: number) => { const DEFAULT_DATA = {
signalEndpointTokens: [],
};
export const useSignalEndpointTokens = (signalEndpointId: number) => {
const { isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks'); const signalsEnabled = useUiFlag('signals');
const { data, error, mutate } = useConditionalSWR( const { data, error, mutate } = useConditionalSWR<{
isEnterprise() && incomingWebhooksEnabled, signalEndpointTokens: ISignalEndpointToken[];
{ incomingWebhookTokens: [] }, }>(
formatApiPath(`${ENDPOINT}/${incomingWebhookId}/tokens`), isEnterprise() && signalsEnabled,
DEFAULT_DATA,
formatApiPath(`${ENDPOINT}/${signalEndpointId}/tokens`),
fetcher, fetcher,
); );
return useMemo( return useMemo(
() => ({ () => ({
incomingWebhookTokens: (data?.incomingWebhookTokens ?? signalEndpointTokens: data?.signalEndpointTokens ?? [],
[]) as IIncomingWebhookToken[],
loading: !error && !data, loading: !error && !data,
refetch: () => mutate(), refetch: () => mutate(),
error, error,
@ -33,6 +38,6 @@ export const useIncomingWebhookTokens = (incomingWebhookId: number) => {
const fetcher = (path: string) => { const fetcher = (path: string) => {
return fetch(path) return fetch(path)
.then(handleErrorResponses('Incoming webhook tokens')) .then(handleErrorResponses('Signal endpoint tokens'))
.then((res) => res.json()); .then((res) => res.json());
}; };

View File

@ -3,23 +3,23 @@ import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler'; import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig'; import useUiConfig from '../useUiConfig/useUiConfig';
import { IIncomingWebhook } from 'interfaces/incomingWebhook'; import { ISignalEndpoint } from 'interfaces/signal';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
const ENDPOINT = 'api/admin/incoming-webhooks'; const ENDPOINT = 'api/admin/signal-endpoints';
const DEFAULT_DATA = { const DEFAULT_DATA = {
incomingWebhooks: [], signalEndpoints: [],
}; };
export const useIncomingWebhooks = () => { export const useSignalEndpoints = () => {
const { isEnterprise } = useUiConfig(); const { isEnterprise } = useUiConfig();
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks'); const signalsEnabled = useUiFlag('signals');
const { data, error, mutate } = useConditionalSWR<{ const { data, error, mutate } = useConditionalSWR<{
incomingWebhooks: IIncomingWebhook[]; signalEndpoints: ISignalEndpoint[];
}>( }>(
isEnterprise() && incomingWebhooksEnabled, isEnterprise() && signalsEnabled,
DEFAULT_DATA, DEFAULT_DATA,
formatApiPath(ENDPOINT), formatApiPath(ENDPOINT),
fetcher, fetcher,
@ -27,7 +27,7 @@ export const useIncomingWebhooks = () => {
return useMemo( return useMemo(
() => ({ () => ({
incomingWebhooks: data?.incomingWebhooks ?? [], signalEndpoints: data?.signalEndpoints ?? [],
loading: !error && !data, loading: !error && !data,
refetch: () => mutate(), refetch: () => mutate(),
error, error,
@ -38,6 +38,6 @@ export const useIncomingWebhooks = () => {
const fetcher = (path: string) => { const fetcher = (path: string) => {
return fetch(path) return fetch(path)
.then(handleErrorResponses('Incoming webhooks')) .then(handleErrorResponses('Signal endpoints'))
.then((res) => res.json()); .then((res) => res.json());
}; };

View File

@ -1,5 +1,10 @@
import { ISignal, SignalSource } from './signal';
import { IConstraint } from './strategy'; import { IConstraint } from './strategy';
type ActionSetState = 'started' | 'success' | 'failed';
type ActionState = ActionSetState | 'not started';
export interface IActionSet { export interface IActionSet {
id: number; id: number;
enabled: boolean; enabled: boolean;
@ -12,15 +17,13 @@ export interface IActionSet {
createdByUserId: number; createdByUserId: number;
} }
type MatchSource = 'incoming-webhook';
export type ParameterMatch = Pick< export type ParameterMatch = Pick<
IConstraint, IConstraint,
'inverted' | 'operator' | 'caseInsensitive' | 'value' | 'values' 'inverted' | 'operator' | 'caseInsensitive' | 'value' | 'values'
>; >;
export interface IMatch { export interface IMatch {
source: MatchSource; source: SignalSource;
sourceId: number; sourceId: number;
payload: Record<string, ParameterMatch>; payload: Record<string, ParameterMatch>;
} }
@ -34,21 +37,6 @@ export interface IAction {
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 { export interface IActionEvent extends IAction {
state: ActionState; state: ActionState;
details?: string; details?: string;
@ -61,9 +49,9 @@ interface IActionSetEventActionSet extends IActionSet {
export interface IActionSetEvent { export interface IActionSetEvent {
id: number; id: number;
actionSetId: number; actionSetId: number;
observableEventId: number; signalId: number;
createdAt: string; createdAt: string;
state: ActionSetState; state: ActionSetState;
observableEvent: IObservableEvent; signal: ISignal;
actionSet: IActionSetEventActionSet; actionSet: IActionSetEventActionSet;
} }

View File

@ -1,28 +0,0 @@
import { ObservableEventSource } from './action';
export interface IIncomingWebhook {
id: number;
enabled: boolean;
name: string;
createdAt: string;
createdByUserId: number;
description: string;
tokens: IIncomingWebhookToken[];
}
export interface IIncomingWebhookToken {
id: number;
name: string;
incomingWebhookId: number;
createdAt: string;
createdByUserId: number;
}
export interface IIncomingWebhookEvent {
id: number;
payload: Record<string, unknown>;
createdAt: string;
source: ObservableEventSource;
sourceId: number;
tokenName: string;
}

View File

@ -0,0 +1,33 @@
export interface ISignalEndpoint {
id: number;
enabled: boolean;
name: string;
createdAt: string;
createdByUserId: number;
description: string;
tokens: ISignalEndpointToken[];
}
export interface ISignalEndpointToken {
id: number;
name: string;
signalEndpointId: number;
createdAt: string;
createdByUserId: number;
}
export type SignalSource = 'signal-endpoint';
export interface ISignal {
id: number;
source: SignalSource;
sourceId: number;
createdAt: string;
createdBySourceTokenId: number;
payload: Record<string, unknown>;
}
export interface ISignalEndpointSignal
extends Omit<ISignal, 'createdBySourceTokenId'> {
tokenName: string;
}

View File

@ -63,7 +63,7 @@ export type UiFlags = {
doraMetrics?: boolean; doraMetrics?: boolean;
dependentFeatures?: boolean; dependentFeatures?: boolean;
newStrategyConfiguration?: boolean; newStrategyConfiguration?: boolean;
incomingWebhooks?: boolean; signals?: boolean;
automatedActions?: boolean; automatedActions?: boolean;
celebrateUnleash?: boolean; celebrateUnleash?: boolean;
increaseUnleashWidth?: boolean; increaseUnleashWidth?: boolean;

View File

@ -117,7 +117,6 @@ exports[`should create default config 1`] = `
"filterInvalidClientMetrics": false, "filterInvalidClientMetrics": false,
"googleAuthEnabled": false, "googleAuthEnabled": false,
"inMemoryScheduledChangeRequests": false, "inMemoryScheduledChangeRequests": false,
"incomingWebhooks": false,
"increaseUnleashWidth": false, "increaseUnleashWidth": false,
"maintenanceMode": false, "maintenanceMode": false,
"messageBanner": { "messageBanner": {
@ -137,6 +136,7 @@ exports[`should create default config 1`] = `
"scimApi": false, "scimApi": false,
"sdkReporting": false, "sdkReporting": false,
"showInactiveUsers": false, "showInactiveUsers": false,
"signals": false,
"strictSchemaValidation": false, "strictSchemaValidation": false,
"stripClientHeadersOn304": false, "stripClientHeadersOn304": false,
"useMemoizedActiveTokens": false, "useMemoizedActiveTokens": false,
@ -176,7 +176,7 @@ exports[`should create default config 1`] = `
"prometheusApi": undefined, "prometheusApi": undefined,
"publicFolder": undefined, "publicFolder": undefined,
"rateLimiting": { "rateLimiting": {
"callIncomingWebhookMaxPerSecond": 1, "callSignalEndpointMaxPerSecond": 1,
"createUserMaxPerMinute": 20, "createUserMaxPerMinute": 20,
"passwordResetMaxPerMinute": 1, "passwordResetMaxPerMinute": 1,
"simpleLoginMaxPerMinute": 10, "simpleLoginMaxPerMinute": 10,

View File

@ -150,8 +150,8 @@ function loadRateLimitingConfig(options: IUnleashOptions): IRateLimiting {
process.env.PASSWORD_RESET_LIMIT_PER_MINUTE, process.env.PASSWORD_RESET_LIMIT_PER_MINUTE,
1, 1,
); );
const callIncomingWebhookMaxPerSecond = parseEnvVarNumber( const callSignalEndpointMaxPerSecond = parseEnvVarNumber(
process.env.INCOMING_WEBHOOK_RATE_LIMIT_PER_SECOND, process.env.SIGNAL_ENDPOINT_RATE_LIMIT_PER_SECOND,
1, 1,
); );
@ -159,7 +159,7 @@ function loadRateLimitingConfig(options: IUnleashOptions): IRateLimiting {
createUserMaxPerMinute, createUserMaxPerMinute,
simpleLoginMaxPerMinute, simpleLoginMaxPerMinute,
passwordResetMaxPerMinute, passwordResetMaxPerMinute,
callIncomingWebhookMaxPerSecond, callSignalEndpointMaxPerSecond,
}; };
return mergeAll([defaultRateLimitOptions, options.rateLimiting || {}]); return mergeAll([defaultRateLimitOptions, options.rateLimiting || {}]);
} }

View File

@ -372,12 +372,11 @@ export default class MetricsMonitor {
.set(config.rateLimiting.passwordResetMaxPerMinute); .set(config.rateLimiting.passwordResetMaxPerMinute);
rateLimits rateLimits
.labels({ .labels({
endpoint: '/api/incoming-webhook/:name', endpoint: '/api/signal-endpoint/:name',
method: 'POST', method: 'POST',
}) })
.set( .set(
config.rateLimiting.callIncomingWebhookMaxPerSecond * config.rateLimiting.callSignalEndpointMaxPerSecond * 60,
60,
); );
} catch (e) {} } catch (e) {}
} }

View File

@ -176,16 +176,16 @@ export const BANNER_CREATED = 'banner-created' as const;
export const BANNER_UPDATED = 'banner-updated' as const; export const BANNER_UPDATED = 'banner-updated' as const;
export const BANNER_DELETED = 'banner-deleted' as const; export const BANNER_DELETED = 'banner-deleted' as const;
export const INCOMING_WEBHOOK_CREATED = 'incoming-webhook-created' as const; export const SIGNAL_ENDPOINT_CREATED = 'signal-endpoint-created' as const;
export const INCOMING_WEBHOOK_UPDATED = 'incoming-webhook-updated' as const; export const SIGNAL_ENDPOINT_UPDATED = 'signal-endpoint-updated' as const;
export const INCOMING_WEBHOOK_DELETED = 'incoming-webhook-deleted' as const; export const SIGNAL_ENDPOINT_DELETED = 'signal-endpoint-deleted' as const;
export const INCOMING_WEBHOOK_TOKEN_CREATED = export const SIGNAL_ENDPOINT_TOKEN_CREATED =
'incoming-webhook-token-created' as const; 'signal-endpoint-token-created' as const;
export const INCOMING_WEBHOOK_TOKEN_UPDATED = export const SIGNAL_ENDPOINT_TOKEN_UPDATED =
'incoming-webhook-token-updated' as const; 'signal-endpoint-token-updated' as const;
export const INCOMING_WEBHOOK_TOKEN_DELETED = export const SIGNAL_ENDPOINT_TOKEN_DELETED =
'incoming-webhook-token-deleted' as const; 'signal-endpoint-token-deleted' as const;
export const ACTIONS_CREATED = 'actions-created' as const; export const ACTIONS_CREATED = 'actions-created' as const;
export const ACTIONS_UPDATED = 'actions-updated' as const; export const ACTIONS_UPDATED = 'actions-updated' as const;
@ -325,12 +325,12 @@ export const IEventTypes = [
PROJECT_ENVIRONMENT_REMOVED, PROJECT_ENVIRONMENT_REMOVED,
DEFAULT_STRATEGY_UPDATED, DEFAULT_STRATEGY_UPDATED,
SEGMENT_IMPORT, SEGMENT_IMPORT,
INCOMING_WEBHOOK_CREATED, SIGNAL_ENDPOINT_CREATED,
INCOMING_WEBHOOK_UPDATED, SIGNAL_ENDPOINT_UPDATED,
INCOMING_WEBHOOK_DELETED, SIGNAL_ENDPOINT_DELETED,
INCOMING_WEBHOOK_TOKEN_CREATED, SIGNAL_ENDPOINT_TOKEN_CREATED,
INCOMING_WEBHOOK_TOKEN_UPDATED, SIGNAL_ENDPOINT_TOKEN_UPDATED,
INCOMING_WEBHOOK_TOKEN_DELETED, SIGNAL_ENDPOINT_TOKEN_DELETED,
ACTIONS_CREATED, ACTIONS_CREATED,
ACTIONS_UPDATED, ACTIONS_UPDATED,
ACTIONS_DELETED, ACTIONS_DELETED,

View File

@ -28,7 +28,7 @@ export type IFlagKey =
| 'disableMetrics' | 'disableMetrics'
| 'stripClientHeadersOn304' | 'stripClientHeadersOn304'
| 'stripHeadersOnAPI' | 'stripHeadersOnAPI'
| 'incomingWebhooks' | 'signals'
| 'automatedActions' | 'automatedActions'
| 'celebrateUnleash' | 'celebrateUnleash'
| 'increaseUnleashWidth' | 'increaseUnleashWidth'
@ -135,8 +135,8 @@ const flags: IFlags = {
.UNLEASH_EXPERIMENTAL_DETECT_SEGMENT_USAGE_IN_CHANGE_REQUESTS, .UNLEASH_EXPERIMENTAL_DETECT_SEGMENT_USAGE_IN_CHANGE_REQUESTS,
false, false,
), ),
incomingWebhooks: parseEnvVarBoolean( signals: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_INCOMING_WEBHOOKS, process.env.UNLEASH_EXPERIMENTAL_SIGNALS,
false, false,
), ),
automatedActions: parseEnvVarBoolean( automatedActions: parseEnvVarBoolean(

View File

@ -207,7 +207,7 @@ export interface IRateLimiting {
createUserMaxPerMinute: number; createUserMaxPerMinute: number;
simpleLoginMaxPerMinute: number; simpleLoginMaxPerMinute: number;
passwordResetMaxPerMinute: number; passwordResetMaxPerMinute: number;
callIncomingWebhookMaxPerSecond: number; callSignalEndpointMaxPerSecond: number;
} }
export interface IUnleashConfig { export interface IUnleashConfig {