mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
Chore inc webhooks modal form (#5938)
https://linear.app/unleash/issue/2-1818/ui-create-incoming-webhook-newedit-modal Adds the incoming webhooks modal form, which allows users to create and edit incoming webhooks, along with their respective tokens. Follows a logic similar to service accounts and their tokens, and tries to use the newest form validation flow that we implemented in the roles form. 
This commit is contained in:
parent
4b02d6aa9c
commit
5b56fac66f
@ -10,7 +10,7 @@ import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Visibility } from '@mui/icons-material';
|
||||
import { BannerDialog } from 'component/banners/Banner/BannerDialog/BannerDialog';
|
||||
|
||||
const StyledForm = styled('form')(({ theme }) => ({
|
||||
const StyledForm = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(4),
|
||||
|
@ -23,13 +23,7 @@ import {
|
||||
IPersonalAPIToken,
|
||||
} from 'interfaces/personalAPIToken';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
useTable,
|
||||
SortingRule,
|
||||
useSortBy,
|
||||
useFlexLayout,
|
||||
Column,
|
||||
} from 'react-table';
|
||||
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { ServiceAccountCreateTokenDialog } from './ServiceAccountCreateTokenDialog/ServiceAccountCreateTokenDialog';
|
||||
import { ServiceAccountTokenDialog } from 'component/admin/serviceAccounts/ServiceAccountsTable/ServiceAccountTokenDialog/ServiceAccountTokenDialog';
|
||||
@ -157,8 +151,7 @@ export const ServiceAccountTokens = ({
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
[
|
||||
() => [
|
||||
{
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
@ -171,10 +164,7 @@ export const ServiceAccountTokens = ({
|
||||
accessor: 'expiresAt',
|
||||
Cell: ({ value }: { value: string }) => {
|
||||
const date = new Date(value);
|
||||
if (
|
||||
date.getFullYear() >
|
||||
new Date().getFullYear() + 100
|
||||
) {
|
||||
if (date.getFullYear() > new Date().getFullYear() + 100) {
|
||||
return <TextCell>Never</TextCell>;
|
||||
}
|
||||
return <DateCell value={value} />;
|
||||
@ -216,7 +206,7 @@ export const ServiceAccountTokens = ({
|
||||
maxWidth: 100,
|
||||
disableSortBy: true,
|
||||
},
|
||||
] as Column<IPersonalAPIToken>[],
|
||||
],
|
||||
[setSelectedToken, setDeleteOpen],
|
||||
);
|
||||
|
||||
@ -228,7 +218,7 @@ export const ServiceAccountTokens = ({
|
||||
|
||||
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
|
||||
{
|
||||
columns,
|
||||
columns: columns as any[],
|
||||
data,
|
||||
initialState,
|
||||
sortTypes,
|
||||
|
@ -0,0 +1,248 @@
|
||||
import {
|
||||
Alert,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
|
||||
import {
|
||||
IncomingWebhooksFormErrors,
|
||||
TokenGeneration,
|
||||
} from './useIncomingWebhooksForm';
|
||||
import { IncomingWebhooksFormURL } from './IncomingWebhooksFormURL';
|
||||
import { IncomingWebhooksTokens } from './IncomingWebhooksTokens/IncomingWebhooksTokens';
|
||||
|
||||
const StyledRaisedSection = styled('div')(({ theme }) => ({
|
||||
background: theme.palette.background.elevation1,
|
||||
padding: theme.spacing(2, 3),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(1),
|
||||
'&:not(:first-of-type)': {
|
||||
marginTop: theme.spacing(4),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledInputSecondaryDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(({ theme }) => ({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
}));
|
||||
|
||||
const StyledSecondarySection = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
marginTop: theme.spacing(4),
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledInlineContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 4),
|
||||
'& > p:not(:first-of-type)': {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
interface IIncomingWebhooksFormProps {
|
||||
incomingWebhook?: IIncomingWebhook;
|
||||
enabled: boolean;
|
||||
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
name: string;
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
description: string;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
tokenGeneration: TokenGeneration;
|
||||
setTokenGeneration: React.Dispatch<React.SetStateAction<TokenGeneration>>;
|
||||
tokenName: string;
|
||||
setTokenName: React.Dispatch<React.SetStateAction<string>>;
|
||||
errors: IncomingWebhooksFormErrors;
|
||||
validateName: (name: string) => boolean;
|
||||
validateTokenName: (
|
||||
tokenGeneration: TokenGeneration,
|
||||
name: string,
|
||||
) => boolean;
|
||||
validated: boolean;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksForm = ({
|
||||
incomingWebhook,
|
||||
enabled,
|
||||
setEnabled,
|
||||
name,
|
||||
setName,
|
||||
description,
|
||||
setDescription,
|
||||
tokenGeneration,
|
||||
setTokenGeneration,
|
||||
tokenName,
|
||||
setTokenName,
|
||||
errors,
|
||||
validateName,
|
||||
validateTokenName,
|
||||
validated,
|
||||
}: IIncomingWebhooksFormProps) => {
|
||||
const handleOnBlur = (callback: Function) => {
|
||||
setTimeout(() => callback(), 300);
|
||||
};
|
||||
|
||||
const showErrors = validated && Object.values(errors).some(Boolean);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IncomingWebhooksFormURL name={name} />
|
||||
<StyledRaisedSection>
|
||||
<FormSwitch checked={enabled} setChecked={setEnabled}>
|
||||
Incoming webhook status
|
||||
</FormSwitch>
|
||||
</StyledRaisedSection>
|
||||
<StyledInputDescription>
|
||||
What is your new incoming webhook name?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
label='Incoming webhook name'
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
validateName(e.target.value);
|
||||
setName(e.target.value);
|
||||
}}
|
||||
onBlur={(e) => handleOnBlur(() => validateName(e.target.value))}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<StyledInputDescription>
|
||||
What is your new incoming webhook description?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
label='Incoming webhook description'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={incomingWebhook === undefined}
|
||||
show={
|
||||
<StyledSecondarySection>
|
||||
<StyledInputDescription>Token</StyledInputDescription>
|
||||
<StyledInputSecondaryDescription>
|
||||
In order to connect your newly created incoming
|
||||
webhook, you will also need a token.{' '}
|
||||
<Link
|
||||
href='https://docs.getunleash.io/reference/api-tokens-and-client-keys'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Read more about API tokens
|
||||
</Link>
|
||||
.
|
||||
</StyledInputSecondaryDescription>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={tokenGeneration}
|
||||
onChange={(e) => {
|
||||
const tokenGeneration = e.target
|
||||
.value as TokenGeneration;
|
||||
|
||||
if (validated) {
|
||||
validateTokenName(
|
||||
tokenGeneration,
|
||||
tokenName,
|
||||
);
|
||||
}
|
||||
setTokenGeneration(tokenGeneration);
|
||||
}}
|
||||
name='token-generation'
|
||||
>
|
||||
<FormControlLabel
|
||||
value={TokenGeneration.LATER}
|
||||
control={<Radio />}
|
||||
label='I want to generate a token later'
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={TokenGeneration.NOW}
|
||||
control={<Radio />}
|
||||
label='Generate a token now'
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<StyledInlineContainer>
|
||||
<StyledInputSecondaryDescription>
|
||||
A new incoming webhook token will be generated
|
||||
for the incoming webhook, so you can get started
|
||||
right away.
|
||||
</StyledInputSecondaryDescription>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
tokenGeneration === TokenGeneration.NOW
|
||||
}
|
||||
show={
|
||||
<>
|
||||
<StyledInputSecondaryDescription>
|
||||
What is your new token name?
|
||||
</StyledInputSecondaryDescription>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
label='Token name'
|
||||
error={Boolean(errors.tokenName)}
|
||||
errorText={errors.tokenName}
|
||||
value={tokenName}
|
||||
onChange={(e) => {
|
||||
validateTokenName(
|
||||
tokenGeneration,
|
||||
e.target.value,
|
||||
);
|
||||
setTokenName(e.target.value);
|
||||
}}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledInlineContainer>
|
||||
</StyledSecondarySection>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<StyledInputDescription>
|
||||
Incoming webhook tokens
|
||||
</StyledInputDescription>
|
||||
<IncomingWebhooksTokens
|
||||
incomingWebhook={incomingWebhook!}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={showErrors}
|
||||
show={() => (
|
||||
<Alert severity='error' icon={false}>
|
||||
<ul>
|
||||
{Object.values(errors)
|
||||
.filter(Boolean)
|
||||
.map((error) => (
|
||||
<li key={error}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,70 @@
|
||||
import { IconButton, Tooltip, styled } from '@mui/material';
|
||||
import CopyIcon from '@mui/icons-material/FileCopy';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
|
||||
const StyledIncomingWebhookUrlSection = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1.5),
|
||||
gap: theme.spacing(1.5),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledIncomingWebhookUrlSectionDescription = styled('p')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledIncomingWebhookUrl = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
padding: theme.spacing(1),
|
||||
width: '100%',
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
interface IIncomingWebhooksFormURLProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksFormURL = ({
|
||||
name,
|
||||
}: IIncomingWebhooksFormURLProps) => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData } = useToast();
|
||||
|
||||
const url = `${uiConfig.unleashUrl}/api/incoming-webhook/${name}`;
|
||||
|
||||
const onCopyToClipboard = () => {
|
||||
copy(url);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledIncomingWebhookUrlSection>
|
||||
<StyledIncomingWebhookUrlSectionDescription>
|
||||
Incoming webhook URL:
|
||||
</StyledIncomingWebhookUrlSectionDescription>
|
||||
<StyledIncomingWebhookUrl>
|
||||
{url}
|
||||
<Tooltip title='Copy URL' arrow>
|
||||
<IconButton onClick={onCopyToClipboard} size='small'>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</StyledIncomingWebhookUrl>
|
||||
</StyledIncomingWebhookUrlSection>
|
||||
);
|
||||
};
|
@ -0,0 +1,305 @@
|
||||
import { Delete } from '@mui/icons-material';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
styled,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { PAT_LIMIT } from '@server/util/constants';
|
||||
import { useIncomingWebhookTokens } from 'hooks/api/getters/useIncomingWebhookTokens/useIncomingWebhookTokens';
|
||||
import { useSearch } from 'hooks/useSearch';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTable, SortingRule, useSortBy, useFlexLayout } from 'react-table';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { IncomingWebhooksTokensCreateDialog } from './IncomingWebhooksTokensCreateDialog';
|
||||
import { IncomingWebhooksTokensDialog } from './IncomingWebhooksTokensDialog';
|
||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import {
|
||||
IncomingWebhookTokenPayload,
|
||||
useIncomingWebhookTokensApi,
|
||||
} from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import {
|
||||
IIncomingWebhook,
|
||||
IIncomingWebhookToken,
|
||||
} from 'interfaces/incomingWebhook';
|
||||
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
||||
|
||||
const StyledHeader = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(2),
|
||||
gap: theme.spacing(2),
|
||||
'& > div': {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginTop: 0,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledTablePlaceholder = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const StyledPlaceholderTitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}));
|
||||
|
||||
const StyledPlaceholderSubtitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
export type PageQueryType = Partial<
|
||||
Record<'sort' | 'order' | 'search', string>
|
||||
>;
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
||||
|
||||
interface IIncomingWebhooksTokensProps {
|
||||
incomingWebhook: IIncomingWebhook;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksTokens = ({
|
||||
incomingWebhook,
|
||||
}: IIncomingWebhooksTokensProps) => {
|
||||
const theme = useTheme();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const { incomingWebhookTokens, refetch: refetchTokens } =
|
||||
useIncomingWebhookTokens(incomingWebhook.id);
|
||||
const { refetch } = useIncomingWebhooks();
|
||||
const { addIncomingWebhookToken, removeIncomingWebhookToken } =
|
||||
useIncomingWebhookTokensApi();
|
||||
|
||||
const [initialState] = useState(() => ({
|
||||
sortBy: [defaultSort],
|
||||
}));
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [tokenOpen, setTokenOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [newToken, setNewToken] = useState('');
|
||||
const [selectedToken, setSelectedToken] = useState<IIncomingWebhookToken>();
|
||||
|
||||
const onCreateClick = async (newToken: IncomingWebhookTokenPayload) => {
|
||||
try {
|
||||
const { token } = await addIncomingWebhookToken(
|
||||
incomingWebhook.id,
|
||||
newToken,
|
||||
);
|
||||
refetch();
|
||||
refetchTokens();
|
||||
setCreateOpen(false);
|
||||
setNewToken(token);
|
||||
setTokenOpen(true);
|
||||
setToastData({
|
||||
title: 'Token created successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteClick = async () => {
|
||||
if (selectedToken) {
|
||||
try {
|
||||
await removeIncomingWebhookToken(
|
||||
incomingWebhook.id,
|
||||
selectedToken.id,
|
||||
);
|
||||
refetch();
|
||||
refetchTokens();
|
||||
setDeleteOpen(false);
|
||||
setToastData({
|
||||
title: 'Token deleted successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
Cell: HighlightCell,
|
||||
minWidth: 100,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
maxWidth: 150,
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
Cell: ({ row: { original: rowToken } }: any) => (
|
||||
<ActionCell>
|
||||
<Tooltip title='Delete token' arrow describeChild>
|
||||
<span>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedToken(rowToken);
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ActionCell>
|
||||
),
|
||||
maxWidth: 100,
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[setSelectedToken, setDeleteOpen],
|
||||
);
|
||||
|
||||
const { data, getSearchText, getSearchContext } = useSearch(
|
||||
columns,
|
||||
searchValue,
|
||||
incomingWebhookTokens,
|
||||
);
|
||||
|
||||
const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
|
||||
{
|
||||
columns: columns as any[],
|
||||
data,
|
||||
initialState,
|
||||
sortTypes,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
disableMultiSort: true,
|
||||
},
|
||||
useSortBy,
|
||||
useFlexLayout,
|
||||
);
|
||||
|
||||
useConditionallyHiddenColumns(
|
||||
[
|
||||
{
|
||||
condition: isSmallScreen,
|
||||
columns: ['createdAt'],
|
||||
},
|
||||
],
|
||||
setHiddenColumns,
|
||||
columns,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledHeader>
|
||||
<Search
|
||||
initialValue={searchValue}
|
||||
onChange={setSearchValue}
|
||||
getSearchContext={getSearchContext}
|
||||
/>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
disabled={incomingWebhookTokens.length >= PAT_LIMIT}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
New token
|
||||
</Button>
|
||||
</StyledHeader>
|
||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={searchValue?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No tokens found matching “
|
||||
{searchValue}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<StyledTablePlaceholder>
|
||||
<StyledPlaceholderTitle>
|
||||
You have no tokens for this incoming webhook
|
||||
yet.
|
||||
</StyledPlaceholderTitle>
|
||||
<StyledPlaceholderSubtitle>
|
||||
Create a token to start using this incoming
|
||||
webhook.
|
||||
</StyledPlaceholderSubtitle>
|
||||
<Button
|
||||
variant='outlined'
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
Create new incoming webhook token
|
||||
</Button>
|
||||
</StyledTablePlaceholder>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<IncomingWebhooksTokensCreateDialog
|
||||
open={createOpen}
|
||||
setOpen={setCreateOpen}
|
||||
tokens={incomingWebhookTokens}
|
||||
onCreateClick={onCreateClick}
|
||||
/>
|
||||
<IncomingWebhooksTokensDialog
|
||||
open={tokenOpen}
|
||||
setOpen={setTokenOpen}
|
||||
token={newToken}
|
||||
/>
|
||||
<Dialogue
|
||||
open={deleteOpen}
|
||||
primaryButtonText='Delete token'
|
||||
secondaryButtonText='Cancel'
|
||||
onClick={onDeleteClick}
|
||||
onClose={() => {
|
||||
setDeleteOpen(false);
|
||||
}}
|
||||
title='Delete token?'
|
||||
>
|
||||
<Typography>
|
||||
Any applications or scripts using this token "
|
||||
<strong>{selectedToken?.name}</strong>" will no longer be
|
||||
able to make requests to this incoming webhook. You cannot
|
||||
undo this action.
|
||||
</Typography>
|
||||
</Dialogue>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { IncomingWebhookTokenPayload } from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi';
|
||||
import { IIncomingWebhookToken } from 'interfaces/incomingWebhook';
|
||||
import { styled } from '@mui/material';
|
||||
import Input from 'component/common/Input/Input';
|
||||
|
||||
const StyledForm = styled('div')(({ theme }) => ({
|
||||
minHeight: theme.spacing(12),
|
||||
}));
|
||||
|
||||
const StyledInputSecondaryDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(({ theme }) => ({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
}));
|
||||
|
||||
interface IIncomingWebhooksTokensCreateDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
tokens: IIncomingWebhookToken[];
|
||||
onCreateClick: (newToken: IncomingWebhookTokenPayload) => void;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksTokensCreateDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
tokens,
|
||||
onCreateClick,
|
||||
}: IIncomingWebhooksTokensCreateDialogProps) => {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const [nameError, setNameError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName('');
|
||||
setNameError('');
|
||||
}, [open]);
|
||||
|
||||
const isNameUnique = (name: string) =>
|
||||
!tokens?.some((token) => token.name === name);
|
||||
|
||||
const validateName = (name: string) => {
|
||||
if (!name.length) {
|
||||
setNameError('Name is required');
|
||||
} else if (!isNameUnique(name)) {
|
||||
setNameError('Name must be unique');
|
||||
} else {
|
||||
setNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = name.length && isNameUnique(name);
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={open}
|
||||
primaryButtonText='Create token'
|
||||
secondaryButtonText='Cancel'
|
||||
onClick={() =>
|
||||
onCreateClick({
|
||||
name,
|
||||
})
|
||||
}
|
||||
disabledPrimaryButton={!isValid}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
title='New token'
|
||||
>
|
||||
<StyledForm>
|
||||
<StyledInputSecondaryDescription>
|
||||
What is your new token name?
|
||||
</StyledInputSecondaryDescription>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
label='Token name'
|
||||
error={Boolean(nameError)}
|
||||
errorText={nameError}
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
validateName(e.target.value);
|
||||
setName(e.target.value);
|
||||
}}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</StyledForm>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { Alert, styled, Typography } from '@mui/material';
|
||||
import { UserToken } from 'component/admin/apiToken/ConfirmToken/UserToken/UserToken';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IIncomingWebhooksTokensDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksTokensDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
token,
|
||||
}: IIncomingWebhooksTokensDialogProps) => (
|
||||
<Dialogue
|
||||
open={open}
|
||||
secondaryButtonText='Close'
|
||||
onClose={(_, muiCloseReason?: string) => {
|
||||
if (!muiCloseReason) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
title='Incoming webhook token created'
|
||||
>
|
||||
<StyledAlert severity='info'>
|
||||
Make sure to copy your incoming webhook token now. You won't be able
|
||||
to see it again!
|
||||
</StyledAlert>
|
||||
<Typography variant='body1'>Your token:</Typography>
|
||||
<UserToken token={token || ''} />
|
||||
</Dialogue>
|
||||
);
|
@ -0,0 +1,136 @@
|
||||
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
||||
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const INCOMING_WEBHOOK_NAME_REGEX = /^[A-Za-z0-9\-_]*$/;
|
||||
|
||||
enum ErrorField {
|
||||
NAME = 'name',
|
||||
TOKEN_NAME = 'tokenName',
|
||||
}
|
||||
|
||||
const DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS = {
|
||||
[ErrorField.NAME]: undefined,
|
||||
[ErrorField.TOKEN_NAME]: undefined,
|
||||
};
|
||||
|
||||
export type IncomingWebhooksFormErrors = Record<ErrorField, string | undefined>;
|
||||
|
||||
export enum TokenGeneration {
|
||||
LATER = 'later',
|
||||
NOW = 'now',
|
||||
}
|
||||
|
||||
export const useIncomingWebhooksForm = (incomingWebhook?: IIncomingWebhook) => {
|
||||
const { incomingWebhooks } = useIncomingWebhooks();
|
||||
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [tokenGeneration, setTokenGeneration] = useState<TokenGeneration>(
|
||||
TokenGeneration.LATER,
|
||||
);
|
||||
const [tokenName, setTokenName] = useState('');
|
||||
|
||||
const reloadForm = () => {
|
||||
setEnabled(incomingWebhook?.enabled ?? true);
|
||||
setName(incomingWebhook?.name || '');
|
||||
setDescription(incomingWebhook?.description || '');
|
||||
setTokenGeneration(TokenGeneration.LATER);
|
||||
setTokenName('');
|
||||
setValidated(false);
|
||||
setErrors(DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reloadForm();
|
||||
}, [incomingWebhook]);
|
||||
|
||||
const [errors, setErrors] = useState<IncomingWebhooksFormErrors>(
|
||||
DEFAULT_INCOMING_WEBHOOKS_FORM_ERRORS,
|
||||
);
|
||||
const [validated, setValidated] = useState(false);
|
||||
|
||||
const clearError = (field: ErrorField) => {
|
||||
setErrors((errors) => ({ ...errors, [field]: undefined }));
|
||||
};
|
||||
|
||||
const setError = (field: ErrorField, error: string) => {
|
||||
setErrors((errors) => ({ ...errors, [field]: error }));
|
||||
};
|
||||
|
||||
const isEmpty = (value: string) => !value.length;
|
||||
|
||||
const isNameNotUnique = (value: string) =>
|
||||
incomingWebhooks?.some(
|
||||
({ id, name }) => id !== incomingWebhook?.id && name === value,
|
||||
);
|
||||
|
||||
const isNameInvalid = (value: string) =>
|
||||
!INCOMING_WEBHOOK_NAME_REGEX.test(value);
|
||||
|
||||
const validateName = (name: string) => {
|
||||
if (isEmpty(name)) {
|
||||
setError(ErrorField.NAME, 'Name is required.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNameNotUnique(name)) {
|
||||
setError(ErrorField.NAME, 'Name must be unique.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNameInvalid(name)) {
|
||||
setError(
|
||||
ErrorField.NAME,
|
||||
'Name must only contain alphanumeric characters, dashes and underscores.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
clearError(ErrorField.NAME);
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateTokenName = (
|
||||
tokenGeneration: TokenGeneration,
|
||||
tokenName: string,
|
||||
) => {
|
||||
if (tokenGeneration === TokenGeneration.NOW && isEmpty(tokenName)) {
|
||||
setError(ErrorField.TOKEN_NAME, 'Token name is required.');
|
||||
return false;
|
||||
}
|
||||
|
||||
clearError(ErrorField.TOKEN_NAME);
|
||||
return true;
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const validName = validateName(name);
|
||||
const validTokenName = validateTokenName(tokenGeneration, tokenName);
|
||||
|
||||
setValidated(true);
|
||||
|
||||
return validName && validTokenName;
|
||||
};
|
||||
|
||||
return {
|
||||
enabled,
|
||||
setEnabled,
|
||||
name,
|
||||
setName,
|
||||
description,
|
||||
setDescription,
|
||||
tokenGeneration,
|
||||
setTokenGeneration,
|
||||
tokenName,
|
||||
setTokenName,
|
||||
errors,
|
||||
setErrors,
|
||||
validated,
|
||||
validateName,
|
||||
validateTokenName,
|
||||
validate,
|
||||
reloadForm,
|
||||
};
|
||||
};
|
@ -0,0 +1,184 @@
|
||||
import { FormEvent, useEffect } from 'react';
|
||||
import { Button, styled } from '@mui/material';
|
||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
|
||||
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
||||
import {
|
||||
IncomingWebhookPayload,
|
||||
useIncomingWebhooksApi,
|
||||
} from 'hooks/api/actions/useIncomingWebhooksApi/useIncomingWebhooksApi';
|
||||
import { useIncomingWebhookTokensApi } from 'hooks/api/actions/useIncomingWebhookTokensApi/useIncomingWebhookTokensApi';
|
||||
import { IncomingWebhooksForm } from './IncomingWebhooksForm/IncomingWebhooksForm';
|
||||
import {
|
||||
TokenGeneration,
|
||||
useIncomingWebhooksForm,
|
||||
} from './IncomingWebhooksForm/useIncomingWebhooksForm';
|
||||
|
||||
const StyledForm = styled('form')(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
paddingTop: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IIncomingWebhooksModalProps {
|
||||
incomingWebhook?: IIncomingWebhook;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
newToken: (token: string) => void;
|
||||
}
|
||||
|
||||
export const IncomingWebhooksModal = ({
|
||||
incomingWebhook,
|
||||
open,
|
||||
setOpen,
|
||||
newToken,
|
||||
}: IIncomingWebhooksModalProps) => {
|
||||
const { refetch } = useIncomingWebhooks();
|
||||
const { addIncomingWebhook, updateIncomingWebhook, loading } =
|
||||
useIncomingWebhooksApi();
|
||||
const { addIncomingWebhookToken } = useIncomingWebhookTokensApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const {
|
||||
enabled,
|
||||
setEnabled,
|
||||
name,
|
||||
setName,
|
||||
description,
|
||||
setDescription,
|
||||
tokenGeneration,
|
||||
setTokenGeneration,
|
||||
tokenName,
|
||||
setTokenName,
|
||||
errors,
|
||||
validateName,
|
||||
validateTokenName,
|
||||
validate,
|
||||
validated,
|
||||
reloadForm,
|
||||
} = useIncomingWebhooksForm(incomingWebhook);
|
||||
|
||||
useEffect(() => {
|
||||
reloadForm();
|
||||
}, [open]);
|
||||
|
||||
const editing = incomingWebhook !== undefined;
|
||||
const title = `${editing ? 'Edit' : 'New'} incoming webhook`;
|
||||
|
||||
const payload: IncomingWebhookPayload = {
|
||||
enabled,
|
||||
name,
|
||||
description,
|
||||
};
|
||||
|
||||
const formatApiCode = () => `curl --location --request ${
|
||||
editing ? 'PUT' : 'POST'
|
||||
} '${uiConfig.unleashUrl}/api/admin/incoming-webhooks${
|
||||
editing ? `/${incomingWebhook.id}` : ''
|
||||
}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validate()) return;
|
||||
|
||||
try {
|
||||
if (editing) {
|
||||
await updateIncomingWebhook(incomingWebhook.id, payload);
|
||||
} else {
|
||||
const { id } = await addIncomingWebhook(payload);
|
||||
if (tokenGeneration === TokenGeneration.NOW) {
|
||||
const { token } = await addIncomingWebhookToken(id, {
|
||||
name: tokenName,
|
||||
});
|
||||
newToken(token);
|
||||
}
|
||||
}
|
||||
setToastData({
|
||||
title: `Incoming webhook ${
|
||||
editing ? 'updated' : 'added'
|
||||
} successfully`,
|
||||
type: 'success',
|
||||
});
|
||||
refetch();
|
||||
setOpen(false);
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
label={title}
|
||||
>
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
modal
|
||||
title={title}
|
||||
description='Incoming Webhooks allow third-party services to send observable events to Unleash.'
|
||||
documentationLink='https://docs.getunleash.io/reference/incoming-webhooks'
|
||||
documentationLinkLabel='Incoming webhooks documentation'
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<IncomingWebhooksForm
|
||||
incomingWebhook={incomingWebhook}
|
||||
enabled={enabled}
|
||||
setEnabled={setEnabled}
|
||||
name={name}
|
||||
setName={setName}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
tokenGeneration={tokenGeneration}
|
||||
setTokenGeneration={setTokenGeneration}
|
||||
tokenName={tokenName}
|
||||
setTokenName={setTokenName}
|
||||
errors={errors}
|
||||
validateName={validateName}
|
||||
validateTokenName={validateTokenName}
|
||||
validated={validated}
|
||||
/>
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
>
|
||||
{editing ? 'Save' : 'Add'} incoming webhook
|
||||
</Button>
|
||||
<StyledCancelButton
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
</StyledButtonContainer>
|
||||
</StyledForm>
|
||||
</FormTemplate>
|
||||
</SidebarModal>
|
||||
);
|
||||
};
|
@ -20,7 +20,8 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { IncomingWebhookTokensCell } from './IncomingWebhooksTokensCell';
|
||||
// import { IncomingWebhooksModal } from '../IncomingWebhooksModal/IncomingWebhooksModal';
|
||||
import { IncomingWebhooksModal } from '../IncomingWebhooksModal/IncomingWebhooksModal';
|
||||
import { IncomingWebhooksTokensDialog } from '../IncomingWebhooksModal/IncomingWebhooksForm/IncomingWebhooksTokens/IncomingWebhooksTokensDialog';
|
||||
|
||||
interface IIncomingWebhooksTableProps {
|
||||
modalOpen: boolean;
|
||||
@ -40,10 +41,12 @@ export const IncomingWebhooksTable = ({
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const { incomingWebhooks, refetch, loading } = useIncomingWebhooks();
|
||||
const { incomingWebhooks, refetch } = useIncomingWebhooks();
|
||||
const { toggleIncomingWebhook, removeIncomingWebhook } =
|
||||
useIncomingWebhooksApi();
|
||||
|
||||
const [tokenDialog, setTokenDialog] = useState(false);
|
||||
const [newToken, setNewToken] = useState('');
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
|
||||
const onToggleIncomingWebhook = async (
|
||||
@ -123,7 +126,7 @@ export const IncomingWebhooksTable = ({
|
||||
/>
|
||||
),
|
||||
searchable: true,
|
||||
maxWidth: 100,
|
||||
maxWidth: 120,
|
||||
},
|
||||
{
|
||||
Header: 'Created',
|
||||
@ -233,11 +236,20 @@ export const IncomingWebhooksTable = ({
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
{/* <IncomingWebhooksModal
|
||||
<IncomingWebhooksModal
|
||||
incomingWebhook={selectedIncomingWebhook}
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
/> */}
|
||||
newToken={(token: string) => {
|
||||
setNewToken(token);
|
||||
setTokenDialog(true);
|
||||
}}
|
||||
/>
|
||||
<IncomingWebhooksTokensDialog
|
||||
open={tokenDialog}
|
||||
setOpen={setTokenDialog}
|
||||
token={newToken}
|
||||
/>
|
||||
<IncomingWebhooksDeleteDialog
|
||||
incomingWebhook={selectedIncomingWebhook}
|
||||
open={deleteOpen}
|
||||
|
@ -103,7 +103,7 @@ export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
|
||||
<IntegrationCard
|
||||
icon='webhook'
|
||||
title='Incoming Webhooks'
|
||||
description='Incoming Webhooks allow third party services to send observable events to Unleash.'
|
||||
description='Incoming Webhooks allow third-party services to send observable events to Unleash.'
|
||||
onClick={onNewIncomingWebhook}
|
||||
/>
|
||||
}
|
||||
|
@ -3,12 +3,12 @@ import useAPI from '../useApi/useApi';
|
||||
|
||||
const ENDPOINT = 'api/admin/incoming-webhooks';
|
||||
|
||||
export type AddOrUpdateIncomingWebhookToken = Omit<
|
||||
export type IncomingWebhookTokenPayload = Omit<
|
||||
IIncomingWebhookToken,
|
||||
'id' | 'incomingWebhookId' | 'createdAt' | 'createdByUserId'
|
||||
>;
|
||||
|
||||
export type IncomingWebhookTokenWithTokenSecret = IIncomingWebhookToken & {
|
||||
type IncomingWebhookTokenWithTokenSecret = IIncomingWebhookToken & {
|
||||
token: string;
|
||||
};
|
||||
|
||||
@ -19,7 +19,7 @@ export const useIncomingWebhookTokensApi = () => {
|
||||
|
||||
const addIncomingWebhookToken = async (
|
||||
incomingWebhookId: number,
|
||||
incomingWebhookToken: AddOrUpdateIncomingWebhookToken,
|
||||
incomingWebhookToken: IncomingWebhookTokenPayload,
|
||||
): Promise<IncomingWebhookTokenWithTokenSecret> => {
|
||||
const requestId = 'addIncomingWebhookToken';
|
||||
const req = createRequest(
|
||||
@ -38,7 +38,7 @@ export const useIncomingWebhookTokensApi = () => {
|
||||
const updateIncomingWebhookToken = async (
|
||||
incomingWebhookId: number,
|
||||
incomingWebhookTokenId: number,
|
||||
incomingWebhookToken: AddOrUpdateIncomingWebhookToken,
|
||||
incomingWebhookToken: IncomingWebhookTokenPayload,
|
||||
) => {
|
||||
const requestId = 'updateIncomingWebhookToken';
|
||||
const req = createRequest(
|
||||
|
@ -3,9 +3,9 @@ import useAPI from '../useApi/useApi';
|
||||
|
||||
const ENDPOINT = 'api/admin/incoming-webhooks';
|
||||
|
||||
export type AddOrUpdateIncomingWebhook = Omit<
|
||||
export type IncomingWebhookPayload = Omit<
|
||||
IIncomingWebhook,
|
||||
'id' | 'createdAt' | 'createdByUserId'
|
||||
'id' | 'createdAt' | 'createdByUserId' | 'tokens'
|
||||
>;
|
||||
|
||||
export const useIncomingWebhooksApi = () => {
|
||||
@ -14,7 +14,7 @@ export const useIncomingWebhooksApi = () => {
|
||||
});
|
||||
|
||||
const addIncomingWebhook = async (
|
||||
incomingWebhook: AddOrUpdateIncomingWebhook,
|
||||
incomingWebhook: IncomingWebhookPayload,
|
||||
) => {
|
||||
const requestId = 'addIncomingWebhook';
|
||||
const req = createRequest(
|
||||
@ -32,7 +32,7 @@ export const useIncomingWebhooksApi = () => {
|
||||
|
||||
const updateIncomingWebhook = async (
|
||||
incomingWebhookId: number,
|
||||
incomingWebhook: AddOrUpdateIncomingWebhook,
|
||||
incomingWebhook: IncomingWebhookPayload,
|
||||
) => {
|
||||
const requestId = 'updateIncomingWebhook';
|
||||
const req = createRequest(
|
||||
|
@ -4,14 +4,16 @@ import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||
import { IIncomingWebhookToken } from 'interfaces/incomingWebhook';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const ENDPOINT = 'api/admin/incoming-webhooks';
|
||||
|
||||
export const useIncomingWebhookTokens = (incomingWebhookId: number) => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR(
|
||||
isEnterprise(),
|
||||
isEnterprise() && incomingWebhooksEnabled,
|
||||
{ incomingWebhookTokens: [] },
|
||||
formatApiPath(`${ENDPOINT}/${incomingWebhookId}/tokens`),
|
||||
fetcher,
|
||||
@ -19,7 +21,7 @@ export const useIncomingWebhookTokens = (incomingWebhookId: number) => {
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
incomingWebhookTokens: (data?.incomingWebhooks ??
|
||||
incomingWebhookTokens: (data?.incomingWebhookTokens ??
|
||||
[]) as IIncomingWebhookToken[],
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
|
Loading…
Reference in New Issue
Block a user