Files
twenty/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
oliver fc287dac78 Feat: API Playground (#10376)
/claim #10283

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2025-03-07 18:03:57 +01:00

272 lines
8.8 KiB
TypeScript

import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import styled from '@emotion/styled';
import { useMemo, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import {
Button,
H2Title,
IconBox,
IconButton,
IconNorthStar,
IconPlus,
IconRefresh,
IconTrash,
Section,
useIcons,
} from 'twenty-ui';
import { AnalyticsActivityGraph } from '@/analytics/components/AnalyticsActivityGraph';
import { AnalyticsGraphEffect } from '@/analytics/components/AnalyticsGraphEffect';
import { AnalyticsGraphDataInstanceContext } from '@/analytics/states/contexts/AnalyticsGraphDataInstanceContext';
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useWebhookUpdateForm } from '@/settings/developers/hooks/useWebhookUpdateForm';
import { SettingsPath } from '@/types/SettingsPath';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const OBJECT_DROPDOWN_WIDTH = 340;
const ACTION_DROPDOWN_WIDTH = 140;
const OBJECT_MOBILE_WIDTH = 150;
const ACTION_MOBILE_WIDTH = 140;
const StyledFilterRow = styled.div<{ isMobile: boolean }>`
display: grid;
grid-template-columns: ${({ isMobile }) =>
isMobile
? `${OBJECT_MOBILE_WIDTH}px ${ACTION_MOBILE_WIDTH}px auto`
: `${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto`};
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
align-items: center;
`;
const StyledPlaceholder = styled.div`
height: ${({ theme }) => theme.spacing(8)};
width: ${({ theme }) => theme.spacing(8)};
`;
export const SettingsDevelopersWebhooksDetail = () => {
const { t } = useLingui();
const { objectMetadataItems } = useObjectMetadataItems();
const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
const isMobile = useIsMobile();
const { getIcon } = useIcons();
const { webhookId = '' } = useParams();
const [searchParams] = useSearchParams();
const isCreationMode = isDefined(searchParams.get('creationMode'));
const {
formData,
title,
loading,
isTargetUrlValid,
updateWebhook,
updateOperation,
removeOperation,
deleteWebhook,
} = useWebhookUpdateForm({
webhookId,
isCreationMode,
});
const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] =
useState(false);
const isAnalyticsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsAnalyticsV2Enabled,
);
const fieldTypeOptions: SelectOption<string>[] = useMemo(
() => [
{ value: '*', label: t`All Objects`, Icon: IconNorthStar },
...objectMetadataItems.map((item) => ({
value: item.nameSingular,
label: item.labelPlural,
Icon: getIcon(item.icon),
})),
],
[objectMetadataItems, getIcon, t],
);
const actionOptions: SelectOption<string>[] = [
{ value: '*', label: t`All Actions`, Icon: IconNorthStar },
{ value: 'created', label: t`Created`, Icon: IconPlus },
{ value: 'updated', label: t`Updated`, Icon: IconRefresh },
{ value: 'deleted', label: t`Deleted`, Icon: IconTrash },
];
if (loading || !formData) {
return <></>;
}
const confirmationText = t`yes`;
return (
<SubMenuTopBarContainer
title={title}
reserveTitleSpace
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: t`Webhook` },
]}
>
<SettingsPageContainer>
<Section>
<H2Title
title={t`Endpoint URL`}
description={t`We will send POST requests to this endpoint for every new event`}
/>
<TextInput
placeholder={t`URL`}
value={formData.targetUrl}
onChange={(targetUrl) => {
updateWebhook({ targetUrl });
}}
error={!isTargetUrlValid ? t`Please enter a valid URL` : undefined}
fullWidth
autoFocus={formData.targetUrl.trim() === ''}
/>
</Section>
<Section>
<H2Title
title={t`Description`}
description={t`An optional description`}
/>
<TextArea
placeholder={t`Write a description`}
minRows={4}
value={formData.description}
onChange={(description) => {
updateWebhook({ description });
}}
/>
</Section>
<Section>
<H2Title
title={t`Filters`}
description={t`Select the events you wish to send to this endpoint`}
/>
{formData.operations.map((operation, index) => (
<StyledFilterRow isMobile={isMobile} key={index}>
<Select
withSearchInput
dropdownWidth={
isMobile ? OBJECT_MOBILE_WIDTH : OBJECT_DROPDOWN_WIDTH
}
dropdownId={`object-webhook-type-select-${index}`}
value={operation.object}
onChange={(object) => updateOperation(index, 'object', object)}
options={fieldTypeOptions}
emptyOption={{
value: null,
label: t`Choose an object`,
Icon: IconBox,
}}
/>
<Select
dropdownWidth={
isMobile ? ACTION_MOBILE_WIDTH : ACTION_DROPDOWN_WIDTH
}
dropdownId={`operation-webhook-type-select-${index}`}
value={operation.action}
onChange={(action) => updateOperation(index, 'action', action)}
options={actionOptions}
/>
{index < formData.operations.length - 1 ? (
<IconButton
onClick={() => removeOperation(index)}
variant="tertiary"
size="medium"
Icon={IconTrash}
/>
) : (
<StyledPlaceholder />
)}
</StyledFilterRow>
))}
</Section>
<Section>
<H2Title
title={t`Secret`}
description={t`Optional: Define a secret string that we will include in every webhook. Use this to authenticate and verify the webhook upon receipt.`}
/>
<TextInput
type="password"
placeholder={t`Write a secret`}
value={formData.secret}
onChange={(secret: string) => {
updateWebhook({ secret: secret.trim() });
}}
fullWidth
/>
</Section>
{!isCreationMode && isAnalyticsEnabled && isAnalyticsV2Enabled && (
<AnalyticsGraphDataInstanceContext.Provider
value={{ instanceId: `webhook-${webhookId}-analytics` }}
>
<AnalyticsGraphEffect
recordId={webhookId}
endpointName="getWebhookAnalytics"
/>
<AnalyticsActivityGraph
recordId={webhookId}
endpointName="getWebhookAnalytics"
/>
</AnalyticsGraphDataInstanceContext.Provider>
)}
<Section>
<H2Title
title={t`Danger zone`}
description={t`Delete this integration`}
/>
<Button
accent="danger"
variant="secondary"
title={t`Delete`}
Icon={IconTrash}
onClick={() => setIsDeleteWebhookModalOpen(true)}
/>
<ConfirmationModal
confirmationPlaceholder={confirmationText}
confirmationValue={confirmationText}
isOpen={isDeleteWebhookModalOpen}
setIsOpen={setIsDeleteWebhookModalOpen}
title={t`Delete webhook`}
subtitle={
<Trans>
Please type {confirmationText} to confirm you want to delete
this webhook.
</Trans>
}
onConfirmClick={deleteWebhook}
confirmButtonText={t`Delete webhook`}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};