diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/webhook.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/webhook.tsx
index d8efb02d832..bce220469a0 100644
--- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/webhook.tsx
+++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/webhook.tsx
@@ -1,9 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
+import { SettingsWebhook } from '@qovery/domains/organizations/feature'
export const Route = createFileRoute('/_authenticated/organization/$organizationId/settings/webhook')({
component: RouteComponent,
})
function RouteComponent() {
- return
Hello "/_authenticated/organization/$organizationId/settings/webhook"!
+ return
}
diff --git a/libs/domains/organizations/feature/src/index.ts b/libs/domains/organizations/feature/src/index.ts
index 08d63d01e2f..116706b3c94 100644
--- a/libs/domains/organizations/feature/src/index.ts
+++ b/libs/domains/organizations/feature/src/index.ts
@@ -92,5 +92,6 @@ export * from './lib/settings-general/settings-general'
export * from './lib/settings-labels-annotations/settings-labels-annotations'
export * from './lib/settings-helm-repositories/settings-helm-repositories'
export * from './lib/settings-container-registries/settings-container-registries'
+export * from './lib/settings-webhook/settings-webhook'
export * from './lib/settings-api-token/settings-api-token'
export * from './lib/settings-danger-zone/settings-danger-zone'
diff --git a/libs/domains/organizations/feature/src/lib/hooks/use-webhooks/use-webhooks.ts b/libs/domains/organizations/feature/src/lib/hooks/use-webhooks/use-webhooks.ts
index 39b4b7f8902..02cb14c2914 100644
--- a/libs/domains/organizations/feature/src/lib/hooks/use-webhooks/use-webhooks.ts
+++ b/libs/domains/organizations/feature/src/lib/hooks/use-webhooks/use-webhooks.ts
@@ -3,11 +3,13 @@ import { queries } from '@qovery/state/util-queries'
export interface UseWebhooksProps {
organizationId: string
+ suspense?: boolean
}
-export function useWebhooks({ organizationId }: UseWebhooksProps) {
+export function useWebhooks({ organizationId, suspense = false }: UseWebhooksProps) {
return useQuery({
...queries.organizations.webhooks({ organizationId }),
+ suspense,
})
}
diff --git a/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.spec.tsx
new file mode 100644
index 00000000000..16d4c221da3
--- /dev/null
+++ b/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.spec.tsx
@@ -0,0 +1,147 @@
+import { webhookFactoryMock } from '@qovery/shared/factories'
+import * as sharedUi from '@qovery/shared/ui'
+import { renderWithProviders, screen, within } from '@qovery/shared/util-tests'
+import * as useDeleteWebhookHook from '../hooks/use-delete-webhook/use-delete-webhook'
+import * as useEditWebhookHook from '../hooks/use-edit-webhook/use-edit-webhook'
+import * as useWebhooksHook from '../hooks/use-webhooks/use-webhooks'
+import { SettingsWebhook } from './settings-webhook'
+import WebhookCrudModal from './webhook-crud-modal/webhook-crud-modal'
+
+jest.mock('@tanstack/react-router', () => ({
+ ...jest.requireActual('@tanstack/react-router'),
+ useParams: () => ({ organizationId: '1' }),
+}))
+
+const useWebhooksMockSpy = jest.spyOn(useWebhooksHook, 'useWebhooks') as jest.Mock
+const useEditWebhookMockSpy = jest.spyOn(useEditWebhookHook, 'useEditWebhook') as jest.Mock
+const useDeleteWebhookMockSpy = jest.spyOn(useDeleteWebhookHook, 'useDeleteWebhook') as jest.Mock
+const useModalMockSpy = jest.spyOn(sharedUi, 'useModal') as jest.Mock
+const useModalConfirmationMockSpy = jest.spyOn(sharedUi, 'useModalConfirmation') as jest.Mock
+
+describe('SettingsWebhook', () => {
+ let openModalMock: jest.Mock
+ let closeModalMock: jest.Mock
+ let openModalConfirmationMock: jest.Mock
+ let editWebhookMock: jest.Mock
+ let deleteWebhookMock: jest.Mock
+
+ const mockWebhooks = webhookFactoryMock(3)
+
+ beforeEach(() => {
+ openModalMock = jest.fn()
+ closeModalMock = jest.fn()
+ openModalConfirmationMock = jest.fn()
+ editWebhookMock = jest.fn().mockResolvedValue(undefined)
+ deleteWebhookMock = jest.fn().mockResolvedValue(undefined)
+
+ useModalMockSpy.mockReturnValue({
+ openModal: openModalMock,
+ closeModal: closeModalMock,
+ enableAlertClickOutside: jest.fn(),
+ })
+ useModalConfirmationMockSpy.mockReturnValue({
+ openModalConfirmation: openModalConfirmationMock,
+ })
+ useWebhooksMockSpy.mockReturnValue({
+ data: mockWebhooks,
+ isLoading: false,
+ })
+ useEditWebhookMockSpy.mockReturnValue({
+ mutateAsync: editWebhookMock,
+ })
+ useDeleteWebhookMockSpy.mockReturnValue({
+ mutateAsync: deleteWebhookMock,
+ })
+ })
+
+ it('should render successfully', () => {
+ const { baseElement } = renderWithProviders( )
+ expect(baseElement).toBeTruthy()
+ })
+
+ it('should render webhooks with actions', () => {
+ renderWithProviders( )
+
+ expect(useWebhooksMockSpy).toHaveBeenCalledWith({ organizationId: '1', suspense: true })
+
+ const rows = screen.getAllByTestId('webhook-row')
+ expect(rows).toHaveLength(3)
+
+ if (mockWebhooks[0].target_url) within(rows[0]).getByText(mockWebhooks[0].target_url)
+ within(rows[0]).getByTestId('edit-webhook')
+ within(rows[0]).getByTestId('input-toggle')
+ within(rows[0]).getByTestId('delete-webhook')
+ })
+
+ it('should open create modal on click on add new', async () => {
+ const { userEvent } = renderWithProviders( )
+
+ await userEvent.click(screen.getByRole('button', { name: 'Add new' }))
+
+ expect(openModalMock).toHaveBeenCalled()
+
+ const modalProps = openModalMock.mock.calls[0][0]
+ expect(modalProps.content.type).toBe(WebhookCrudModal)
+ expect(modalProps.content.props.organizationId).toBe('1')
+ expect(modalProps.content.props.webhook).toBeUndefined()
+ })
+
+ it('should open edit modal on click on cog', async () => {
+ const { userEvent } = renderWithProviders( )
+
+ await userEvent.click(screen.getAllByTestId('edit-webhook')[0])
+
+ expect(openModalMock).toHaveBeenCalled()
+
+ const modalProps = openModalMock.mock.calls[0][0]
+ expect(modalProps.content.type).toBe(WebhookCrudModal)
+ expect(modalProps.content.props.webhook).toEqual(mockWebhooks[0])
+ })
+
+ it('should toggle a webhook', async () => {
+ const { userEvent } = renderWithProviders( )
+
+ const rows = screen.getAllByTestId('webhook-row')
+ const toggle = within(rows[0]).getByTestId('input-toggle-button')
+
+ await userEvent.click(toggle)
+
+ expect(editWebhookMock).toHaveBeenCalledWith({
+ organizationId: '1',
+ webhookId: mockWebhooks[0].id,
+ webhookRequest: {
+ ...mockWebhooks[0],
+ enabled: true,
+ },
+ })
+ expect(closeModalMock).toHaveBeenCalled()
+ })
+
+ it('should display empty placeholder', () => {
+ useWebhooksMockSpy.mockReturnValue({
+ data: [],
+ })
+
+ renderWithProviders( )
+
+ expect(useWebhooksMockSpy).toHaveBeenCalledWith({ organizationId: '1', suspense: true })
+
+ screen.getByTestId('empty-webhook')
+ })
+
+ it('should call delete action from confirmation modal', async () => {
+ const { userEvent } = renderWithProviders( )
+
+ await userEvent.click(screen.getAllByTestId('delete-webhook')[0])
+
+ expect(openModalConfirmationMock).toHaveBeenCalled()
+
+ const modalProps = openModalConfirmationMock.mock.calls[0][0]
+ modalProps.action()
+
+ expect(deleteWebhookMock).toHaveBeenCalledWith({
+ organizationId: '1',
+ webhookId: mockWebhooks[0].id,
+ })
+ })
+})
diff --git a/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.tsx b/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.tsx
new file mode 100644
index 00000000000..43c127fccc9
--- /dev/null
+++ b/libs/domains/organizations/feature/src/lib/settings-webhook/settings-webhook.tsx
@@ -0,0 +1,200 @@
+import { useParams } from '@tanstack/react-router'
+import { OrganizationWebhookKindEnum, type OrganizationWebhookResponse } from 'qovery-typescript-axios'
+import { Suspense } from 'react'
+import { SettingsHeading } from '@qovery/shared/console-shared'
+import { IconEnum } from '@qovery/shared/enums'
+import { useModal, useModalConfirmation } from '@qovery/shared/ui'
+import { BlockContent, Button, Icon, InputToggle, Section, Skeleton, Tooltip, Truncate } from '@qovery/shared/ui'
+import { dateUTCString, timeAgo } from '@qovery/shared/util-dates'
+import { useDocumentTitle } from '@qovery/shared/util-hooks'
+import { upperCaseFirstLetter } from '@qovery/shared/util-js'
+import { useDeleteWebhook } from '../hooks/use-delete-webhook/use-delete-webhook'
+import { useEditWebhook } from '../hooks/use-edit-webhook/use-edit-webhook'
+import { useWebhooks } from '../hooks/use-webhooks/use-webhooks'
+import WebhookCrudModal from './webhook-crud-modal/webhook-crud-modal'
+
+const WebhookSkeleton = () => (
+
+ {[0, 1, 2].map((index) => (
+
+ ))}
+
+)
+
+const WebhookList = ({ organizationId }: { organizationId: string }) => {
+ const fetchWebhooks = useWebhooks({ organizationId, suspense: true })
+ const { mutateAsync: deleteWebhook } = useDeleteWebhook()
+ const { openModal, closeModal } = useModal()
+ const { openModalConfirmation } = useModalConfirmation()
+ const { mutateAsync: editWebhook } = useEditWebhook()
+ const webhooks = fetchWebhooks.data ?? []
+
+ const openEdit = (webhook: OrganizationWebhookResponse) => {
+ openModal({
+ content: ,
+ })
+ }
+
+ const onDelete = (webhook: OrganizationWebhookResponse) => {
+ openModalConfirmation({
+ title: 'Delete webhook',
+ confirmationMethod: 'action',
+ name: webhook.target_url || '',
+ action: () => {
+ try {
+ deleteWebhook({ organizationId, webhookId: webhook.id })
+ } catch (error) {
+ console.error(error)
+ }
+ },
+ })
+ }
+
+ const toggleWebhook = (webhookId: string, enabled: boolean) => {
+ // this cast as Required is there to fix an incoherency in the api-doc. If request have all the field required
+ // then the Response must also have all the fields defined
+ const webhook = fetchWebhooks.data?.find(
+ (webhook) => webhook.id === webhookId
+ ) as Required
+
+ if (webhook !== undefined) {
+ try {
+ editWebhook({
+ organizationId,
+ webhookId,
+ webhookRequest: {
+ ...webhook,
+ enabled: enabled,
+ },
+ })
+ closeModal()
+ } catch (error) {
+ console.error(error)
+ }
+ }
+ }
+
+ return (
+
+ {webhooks.length > 0 ? (
+
+ ) : (
+
+
+
+ No webhook found. Please add one.
+
+
+ )}
+
+ )
+}
+
+export function SettingsWebhook() {
+ useDocumentTitle('Webhooks - Organization settings')
+ const { organizationId = '' } = useParams({ strict: false })
+ const { openModal, closeModal } = useModal()
+
+ const openAddNew = () => {
+ openModal({
+ content: ,
+ })
+ }
+
+ return (
+
+
+
+
+
+
+ Add new
+
+
+
+ }>
+
+
+
+
+
+ )
+}
diff --git a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.spec.tsx
similarity index 81%
rename from libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.spec.tsx
rename to libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.spec.tsx
index 0883efe6328..49f5566b550 100644
--- a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.spec.tsx
+++ b/libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.spec.tsx
@@ -1,8 +1,9 @@
import selectEvent from 'react-select-event'
-import * as organizationDomain from '@qovery/domains/organizations/feature'
import { webhookFactoryMock } from '@qovery/shared/factories'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
-import WebhookCrudModalFeature, { type WebhookCrudModalFeatureProps } from './webhook-crud-modal-feature'
+import * as useCreateWebhookHook from '../../hooks/use-create-webhook/use-create-webhook'
+import * as useEditWebhookHook from '../../hooks/use-edit-webhook/use-edit-webhook'
+import WebhookCrudModal, { type WebhookCrudModalFeatureProps } from './webhook-crud-modal'
const mockWebhook = webhookFactoryMock(1)[0]
const mockWebhookWithSecret = { ...mockWebhook, target_secret_set: true }
@@ -12,26 +13,33 @@ const props: WebhookCrudModalFeatureProps = {
webhook: undefined,
}
-const useEditWebhooksMockSpy = jest.spyOn(organizationDomain, 'useEditWebhook') as jest.Mock
-const useCreateWebhookMockSpy = jest.spyOn(organizationDomain, 'useCreateWebhook') as jest.Mock
+const useEditWebhookMockSpy = jest.spyOn(useEditWebhookHook, 'useEditWebhook') as jest.Mock
+const useCreateWebhookMockSpy = jest.spyOn(useCreateWebhookHook, 'useCreateWebhook') as jest.Mock
+
+describe('WebhookCrudModal', () => {
+ let createWebhookMock: jest.Mock
+ let editWebhookMock: jest.Mock
-describe('WebhookCrudModalFeature', () => {
beforeEach(() => {
- useEditWebhooksMockSpy.mockReturnValue({
- mutateAsync: jest.fn(),
+ createWebhookMock = jest.fn().mockResolvedValue(undefined)
+ editWebhookMock = jest.fn().mockResolvedValue(undefined)
+ useEditWebhookMockSpy.mockReturnValue({
+ mutateAsync: editWebhookMock,
+ isLoading: false,
})
useCreateWebhookMockSpy.mockReturnValue({
- mutateAsync: jest.fn(),
+ mutateAsync: createWebhookMock,
+ isLoading: false,
})
})
it('should render successfully', () => {
- const { baseElement } = renderWithProviders( )
+ const { baseElement } = renderWithProviders( )
expect(baseElement).toBeTruthy()
})
it('should render all the inputs when creating', () => {
- renderWithProviders( )
+ renderWithProviders( )
screen.getByLabelText('URL')
screen.getByLabelText('Kind')
@@ -43,7 +51,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should not show secret field when editing (before form is dirty)', () => {
- renderWithProviders( )
+ renderWithProviders( )
screen.getByLabelText('URL')
screen.getByLabelText('Kind')
@@ -53,7 +61,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should mutate useCreateWebhook', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const url = screen.getByLabelText('URL')
const kind = screen.getByLabelText('Kind')
@@ -88,7 +96,7 @@ describe('WebhookCrudModalFeature', () => {
await userEvent.click(button)
- expect(useCreateWebhookMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(createWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookRequest: {
target_url: 'https://test.com',
@@ -103,7 +111,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should mutate useEditWebhook (webhook without existing secret)', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const url = screen.getByLabelText('URL')
const kind = screen.getByLabelText('Kind')
const description = screen.getByLabelText('Description')
@@ -132,7 +140,7 @@ describe('WebhookCrudModalFeature', () => {
await userEvent.click(button)
- expect(useEditWebhooksMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(editWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookId: mockWebhook.id,
webhookRequest: {
@@ -149,7 +157,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should trim URL with trailing whitespace on create', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const url = screen.getByLabelText('URL')
const kind = screen.getByLabelText('Kind')
@@ -169,7 +177,7 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useCreateWebhookMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(createWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookRequest: expect.objectContaining({
target_url: 'https://test.com',
@@ -178,7 +186,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should trim URL with leading whitespace on create', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const url = screen.getByLabelText('URL')
const kind = screen.getByLabelText('Kind')
@@ -198,7 +206,7 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useCreateWebhookMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(createWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookRequest: expect.objectContaining({
target_url: 'https://test.com',
@@ -207,7 +215,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should trim URL with whitespace on edit', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const url = screen.getByLabelText('URL')
@@ -218,7 +226,7 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useEditWebhooksMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(editWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookId: mockWebhook.id,
webhookRequest: expect.objectContaining({
@@ -229,20 +237,20 @@ describe('WebhookCrudModalFeature', () => {
describe('secret field handling', () => {
it('should show secret field as optional when creating new webhook', () => {
- renderWithProviders( )
+ renderWithProviders( )
expect(screen.getByLabelText('Secret')).toBeInTheDocument()
})
it('should not show secret field when editing webhook (before form is dirty)', () => {
- renderWithProviders( )
+ renderWithProviders( )
expect(screen.queryByLabelText('Secret')).not.toBeInTheDocument()
expect(screen.queryByText('Confirm your secret')).not.toBeInTheDocument()
})
it('should show "Confirm your secret" section when editing webhook with existing secret and form is dirty', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
expect(screen.queryByText('Confirm your secret')).not.toBeInTheDocument()
@@ -255,7 +263,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should not show secret field when editing webhook without existing secret (even when dirty)', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const description = screen.getByLabelText('Description')
await userEvent.clear(description)
@@ -266,7 +274,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should require secret when editing webhook with existing secret', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const description = screen.getByLabelText('Description')
await userEvent.clear(description)
@@ -278,11 +286,11 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useEditWebhooksMockSpy().mutateAsync).not.toHaveBeenCalled()
+ expect(editWebhookMock).not.toHaveBeenCalled()
})
it('should allow submit when editing webhook with existing secret and secret provided', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const description = screen.getByLabelText('Description')
await userEvent.clear(description)
@@ -294,7 +302,7 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useEditWebhooksMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(editWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookId: mockWebhookWithSecret.id,
webhookRequest: expect.objectContaining({
@@ -304,7 +312,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should display password type input for secret field when shown', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const description = screen.getByLabelText('Description')
await userEvent.type(description, 'x')
@@ -314,7 +322,7 @@ describe('WebhookCrudModalFeature', () => {
})
it('should allow editing webhook without secret and not providing a secret', async () => {
- const { userEvent } = renderWithProviders( )
+ const { userEvent } = renderWithProviders( )
const description = screen.getByLabelText('Description')
await userEvent.clear(description)
@@ -323,7 +331,7 @@ describe('WebhookCrudModalFeature', () => {
const button = screen.getByTestId('submit-button')
await userEvent.click(button)
- expect(useEditWebhooksMockSpy().mutateAsync).toHaveBeenCalledWith({
+ expect(editWebhookMock).toHaveBeenCalledWith({
organizationId: '000-000-000',
webhookId: mockWebhook.id,
webhookRequest: expect.objectContaining({
diff --git a/libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.tsx b/libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.tsx
new file mode 100644
index 00000000000..5e060f55c45
--- /dev/null
+++ b/libs/domains/organizations/feature/src/lib/settings-webhook/webhook-crud-modal/webhook-crud-modal.tsx
@@ -0,0 +1,292 @@
+import {
+ EnvironmentModeEnum,
+ type OrganizationWebhookCreateRequest,
+ OrganizationWebhookEventEnum,
+ OrganizationWebhookKindEnum,
+ type OrganizationWebhookResponse,
+} from 'qovery-typescript-axios'
+import { type FormEventHandler } from 'react'
+import { FormProvider, useForm } from 'react-hook-form'
+import { Controller } from 'react-hook-form'
+import { IconEnum } from '@qovery/shared/enums'
+import { useModal } from '@qovery/shared/ui'
+import { Icon, InputSelect, InputTags, InputText, InputTextArea, ModalCrud } from '@qovery/shared/ui'
+import { useCreateWebhook } from '../../hooks/use-create-webhook/use-create-webhook'
+import { useEditWebhook } from '../../hooks/use-edit-webhook/use-edit-webhook'
+
+export interface WebhookCrudModalFeatureProps {
+ organizationId: string
+ closeModal: () => void
+ webhook?: OrganizationWebhookResponse
+}
+
+export function WebhookCrudModal({ organizationId, closeModal, webhook }: WebhookCrudModalFeatureProps) {
+ const { enableAlertClickOutside } = useModal()
+ const { mutateAsync: createWebhook, isLoading: isLoadingCreateWebhook } = useCreateWebhook()
+ const { mutateAsync: editWebhook, isLoading: isLoadingEditWebhook } = useEditWebhook()
+ const isEdit = Boolean(webhook)
+ const hasExistingSecret = Boolean(webhook?.target_secret_set)
+
+ const methods = useForm({
+ mode: 'all',
+ defaultValues: {
+ kind: webhook?.kind ?? undefined,
+ environment_types_filter: webhook?.environment_types_filter ?? [],
+ project_names_filter: webhook?.project_names_filter ?? [],
+ events: webhook?.events ?? [],
+ description: webhook?.description ?? '',
+ target_url: webhook?.target_url ?? '',
+ target_secret: '',
+ },
+ })
+
+ methods.watch(() => enableAlertClickOutside(methods.formState.isDirty))
+
+ const isEditDirty = isEdit && methods.formState.isDirty
+
+ const onSubmit = methods.handleSubmit(async (data) => {
+ const trimmedData = {
+ ...data,
+ target_url: data.target_url.trim(),
+ target_secret: data.target_secret || undefined,
+ }
+ try {
+ if (webhook) {
+ await editWebhook({
+ organizationId,
+ webhookId: webhook.id,
+ webhookRequest: { ...webhook, ...trimmedData },
+ })
+ closeModal()
+ } else {
+ await createWebhook({
+ organizationId,
+ webhookRequest: trimmedData,
+ })
+ closeModal()
+ }
+ } catch (error) {
+ console.error(error)
+ }
+ })
+
+ return (
+
+
+ General
+
+ (
+
+ )}
+ />
+
+ (
+ ,
+ },
+ {
+ label: 'Standard',
+ value: OrganizationWebhookKindEnum.STANDARD,
+ icon: ,
+ },
+ ]}
+ onChange={field.onChange}
+ value={field.value}
+ label="Kind"
+ error={error?.message}
+ />
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+ {!isEdit && (
+ (
+
+ )}
+ />
+ )}
+
+ Event & filters
+
+
+ (
+
+ )}
+ />
+
+
+
+
(
+
+ )}
+ />
+
+ Webhook will be triggered only for projects whose names match or, if you're using a wildcard, start with one
+ of the values from your list.
+
+ Press Enter to add a new value.
+
+
+
+
+ (
+
+ )}
+ />
+
+
+ {isEditDirty && hasExistingSecret && (
+ <>
+
+ Confirm your secret
+ (
+
+ )}
+ />
+ >
+ )}
+
+
+ )
+}
+
+export default WebhookCrudModal
diff --git a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.spec.tsx b/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.spec.tsx
deleted file mode 100644
index dc453ad3bd6..00000000000
--- a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.spec.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import * as organizationsDomain from '@qovery/domains/organizations/feature'
-import { webhookFactoryMock } from '@qovery/shared/factories'
-import { renderWithProviders, screen } from '@qovery/shared/util-tests'
-import PageOrganizationWebhooksFeature from './page-organization-webhooks-feature'
-
-const useWebhooksMockSpy = jest.spyOn(organizationsDomain, 'useWebhooks') as jest.Mock
-const useEditWebhooksMockSpy = jest.spyOn(organizationsDomain, 'useEditWebhook') as jest.Mock
-
-const mockWebhooks = webhookFactoryMock(1)
-
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useParams: () => ({ organizationId: '1' }),
-}))
-
-describe('PageOrganizationWebhooksFeature', () => {
- beforeEach(() => {
- useWebhooksMockSpy.mockReturnValue({
- data: mockWebhooks,
- })
- useEditWebhooksMockSpy.mockReturnValue({
- mutateAsync: jest.fn(),
- })
- })
-
- it('should render successfully', () => {
- const { baseElement } = renderWithProviders( )
- expect(baseElement).toBeTruthy()
- })
-
- it('should render 1 row', () => {
- renderWithProviders( )
- const rows = screen.getAllByTestId('webhook-row')
- expect(rows).toHaveLength(1)
- })
-
- it('should render empty placeholder', () => {
- useWebhooksMockSpy.mockReturnValue({
- data: [],
- })
- renderWithProviders( )
- screen.getByTestId('empty-webhook')
- })
-
- it('should render pass the toggle to true', async () => {
- const { userEvent } = renderWithProviders( )
-
- const input = screen.getByTestId('input-toggle-button')
- await userEvent.click(input)
-
- expect(useEditWebhooksMockSpy().mutateAsync).toHaveBeenCalledWith({
- organizationId: '1',
- webhookId: mockWebhooks[0].id,
- webhookRequest: {
- ...mockWebhooks[0],
- enabled: true,
- },
- })
- })
-})
diff --git a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.tsx
deleted file mode 100644
index 249080e4ff8..00000000000
--- a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/page-organization-webhooks-feature.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { type OrganizationWebhookResponse } from 'qovery-typescript-axios'
-import { useParams } from 'react-router-dom'
-import { useDeleteWebhook, useEditWebhook, useWebhooks } from '@qovery/domains/organizations/feature'
-import { useModal, useModalConfirmation } from '@qovery/shared/ui'
-import { useDocumentTitle } from '@qovery/shared/util-hooks'
-import PageOrganizationWebhooks from '../../ui/page-organization-webhooks/page-organization-webhooks'
-import WebhookCrudModalFeature from './webhook-crud-modal-feature/webhook-crud-modal-feature'
-
-export function PageOrganizationWebhooksFeature() {
- useDocumentTitle('Webhooks - Organization settings')
- const { organizationId = '' } = useParams()
- const fetchWebhooks = useWebhooks({ organizationId })
- const { mutateAsync: deleteWebhook } = useDeleteWebhook()
- const { openModal, closeModal } = useModal()
- const { openModalConfirmation } = useModalConfirmation()
- const { mutateAsync: editWebhook } = useEditWebhook()
-
- const openAddNew = () => {
- openModal({
- content: ,
- })
- }
-
- const openEdit = (webhook: OrganizationWebhookResponse) => {
- openModal({
- content: ,
- })
- }
-
- const onDelete = (webhook: OrganizationWebhookResponse) => {
- openModalConfirmation({
- title: 'Delete webhook',
- confirmationMethod: 'action',
- name: webhook.target_url || '',
- action: () => {
- try {
- deleteWebhook({ organizationId, webhookId: webhook.id })
- } catch (error) {
- console.error(error)
- }
- },
- })
- }
-
- const toggleWebhook = (webhookId: string, enabled: boolean) => {
- // this cast as Required is there to fix an incoherency in the api-doc. If request have all the field required
- // then the Response must also have all the fields defined
- const webhook = fetchWebhooks.data?.find(
- (webhook) => webhook.id === webhookId
- ) as Required
-
- if (webhook !== undefined) {
- try {
- editWebhook({
- organizationId,
- webhookId,
- webhookRequest: {
- ...webhook,
- enabled: enabled,
- },
- })
- closeModal()
- } catch (error) {
- console.error(error)
- }
- }
- }
-
- return (
-
- )
-}
-
-export default PageOrganizationWebhooksFeature
diff --git a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.tsx b/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.tsx
deleted file mode 100644
index 453f4f8038d..00000000000
--- a/libs/pages/settings/src/lib/feature/page-organization-webhooks-feature/webhook-crud-modal-feature/webhook-crud-modal-feature.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { type OrganizationWebhookCreateRequest, type OrganizationWebhookResponse } from 'qovery-typescript-axios'
-import { FormProvider, useForm } from 'react-hook-form'
-import { useCreateWebhook, useEditWebhook } from '@qovery/domains/organizations/feature'
-import { useModal } from '@qovery/shared/ui'
-import WebhookCrudModal from '../../../ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal'
-
-export interface WebhookCrudModalFeatureProps {
- organizationId: string
- closeModal: () => void
- webhook?: OrganizationWebhookResponse
-}
-
-export function WebhookCrudModalFeature({ organizationId, closeModal, webhook }: WebhookCrudModalFeatureProps) {
- const { enableAlertClickOutside } = useModal()
- const { mutateAsync: createWebhook, isLoading: isLoadingCreateWebhook } = useCreateWebhook()
- const { mutateAsync: editWebhook, isLoading: isLoadingEditWebhook } = useEditWebhook()
-
- const isEdit = Boolean(webhook)
- const hasExistingSecret = Boolean(webhook?.target_secret_set)
-
- const methods = useForm({
- mode: 'all',
- defaultValues: {
- kind: webhook?.kind ?? undefined,
- environment_types_filter: webhook?.environment_types_filter ?? [],
- project_names_filter: webhook?.project_names_filter ?? [],
- events: webhook?.events ?? [],
- description: webhook?.description ?? '',
- target_url: webhook?.target_url ?? '',
- target_secret: '',
- },
- })
-
- methods.watch(() => enableAlertClickOutside(methods.formState.isDirty))
-
- const isEditDirty = isEdit && methods.formState.isDirty
-
- const onSubmit = methods.handleSubmit(async (data) => {
- const trimmedData = {
- ...data,
- target_url: data.target_url.trim(),
- target_secret: data.target_secret || undefined,
- }
- try {
- if (webhook) {
- await editWebhook({
- organizationId,
- webhookId: webhook.id,
- webhookRequest: { ...webhook, ...trimmedData },
- })
- closeModal()
- } else {
- await createWebhook({
- organizationId,
- webhookRequest: trimmedData,
- })
- closeModal()
- }
- } catch (error) {
- console.error(error)
- }
- })
-
- return (
-
-
-
- )
-}
-
-export default WebhookCrudModalFeature
diff --git a/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.spec.tsx b/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.spec.tsx
deleted file mode 100644
index f497259ab56..00000000000
--- a/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.spec.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { webhookFactoryMock } from '@qovery/shared/factories'
-import { renderWithProviders, screen, within } from '@qovery/shared/util-tests'
-import PageOrganizationWebhooks, { type PageOrganizationWebhooksProps } from './page-organization-webhooks'
-
-const mockWebhooks = webhookFactoryMock(3)
-const props: PageOrganizationWebhooksProps = {
- openEdit: jest.fn(),
- onToggle: jest.fn(),
- webhooks: mockWebhooks,
- webhookLoading: false,
- openAddNew: jest.fn(),
- onDelete: jest.fn(),
-}
-
-describe('PageOrganizationWebhooks', () => {
- it('should render successfully', () => {
- const { baseElement } = renderWithProviders( )
- expect(baseElement).toBeTruthy()
- })
-
- it('should open create modal on click on add new', async () => {
- const { userEvent } = renderWithProviders( )
- const button = screen.getByTestId('add-new')
-
- await userEvent.click(button)
-
- expect(props.openAddNew).toHaveBeenCalled()
- })
-
- it('should open edit modal on click on cog', async () => {
- const { userEvent } = renderWithProviders( )
- const button = screen.getAllByTestId('edit-webhook')[0]
-
- await userEvent.click(button)
-
- expect(props.openEdit).toHaveBeenCalledWith(mockWebhooks[0])
- })
-
- it('should display three rows with good values inside', async () => {
- renderWithProviders( )
- const rows = screen.getAllByTestId('webhook-row')
- expect(rows).toHaveLength(3)
-
- if (mockWebhooks[0].target_url) within(rows[0]).getByText(mockWebhooks[0].target_url)
- within(rows[0]).getByTestId('edit-webhook')
- within(rows[0]).getByTestId('input-toggle')
- within(rows[0]).getByTestId('delete-webhook')
- })
-
- it('should call onToggle', async () => {
- const { userEvent } = renderWithProviders( )
- const toggles = screen.getAllByTestId('input-toggle')
-
- const input = within(toggles[0]).getByLabelText('toggle-btn')
- await userEvent.click(input)
-
- expect(props.onToggle).toHaveBeenCalledWith(mockWebhooks[0].id, !mockWebhooks[0].enabled)
- })
-
- it('should display empty placeholder', async () => {
- renderWithProviders( )
-
- screen.getByTestId('empty-webhook')
- })
-
- it('should call onDelete', async () => {
- const { userEvent } = renderWithProviders( )
- const rows = screen.getAllByTestId('webhook-row')
-
- const toggle = within(rows[0]).getByTestId('delete-webhook')
- await userEvent.click(toggle)
-
- expect(props.onDelete).toHaveBeenCalledWith(mockWebhooks[0])
- })
-})
diff --git a/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.tsx b/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.tsx
deleted file mode 100644
index 2bfcf733609..00000000000
--- a/libs/pages/settings/src/lib/ui/page-organization-webhooks/page-organization-webhooks.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import { OrganizationWebhookKindEnum, type OrganizationWebhookResponse } from 'qovery-typescript-axios'
-import { NeedHelp } from '@qovery/shared/assistant/feature'
-import { IconEnum } from '@qovery/shared/enums'
-import {
- BlockContent,
- Button,
- Heading,
- Icon,
- InputToggle,
- LoaderSpinner,
- Section,
- Tooltip,
- Truncate,
-} from '@qovery/shared/ui'
-import { dateUTCString, timeAgo } from '@qovery/shared/util-dates'
-import { upperCaseFirstLetter } from '@qovery/shared/util-js'
-
-export interface PageOrganizationWebhooksProps {
- webhookLoading: boolean
- webhooks?: OrganizationWebhookResponse[]
- openAddNew: () => void
- openEdit: (webhook: OrganizationWebhookResponse) => void
- onToggle: (id: string, enabled: boolean) => void
- onDelete: (webhook: OrganizationWebhookResponse) => void
-}
-
-export function PageOrganizationWebhooks(props: PageOrganizationWebhooksProps) {
- return (
-
-
-
-
-
Webhook
-
- Qovery allows you to create webhooks at organization-level so that, when an event happens on an
- environment within your organization, you can get notified on external applications (for instance, Slack).
-
-
-
-
- Add new
-
-
-
-
- {props.webhookLoading ? (
-
-
-
- ) : props.webhooks && props.webhooks?.length > 0 ? (
-
- ) : (
-
-
-
- No webhook found. Please add one.
-
-
- )}
-
-
-
- )
-}
-
-export default PageOrganizationWebhooks
diff --git a/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.spec.tsx b/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.spec.tsx
deleted file mode 100644
index 8726be9b86e..00000000000
--- a/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.spec.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
-import {
- EnvironmentModeEnum,
- type OrganizationWebhookCreateRequest,
- OrganizationWebhookEventEnum,
- OrganizationWebhookKindEnum,
-} from 'qovery-typescript-axios'
-import { fireEvent, renderWithProviders, screen } from '@qovery/shared/util-tests'
-import WebhookCrudModal, { type WebhookCrudModalProps } from './webhook-crud-modal'
-
-const props: WebhookCrudModalProps = {
- onSubmit: jest.fn((e) => e.preventDefault()),
- isEdition: false,
- closeModal: jest.fn(),
-}
-
-describe('WebhookCrudModal', () => {
- const defaultValues: OrganizationWebhookCreateRequest = {
- target_url: 'https://test.com',
- kind: OrganizationWebhookKindEnum.STANDARD,
- description: 'description',
- target_secret: 'secret',
- events: [OrganizationWebhookEventEnum.STARTED],
- project_names_filter: ['test'],
- environment_types_filter: [EnvironmentModeEnum.PRODUCTION],
- }
- it('should render successfully', () => {
- const { baseElement } = renderWithProviders(
- wrapWithReactHookForm( , {
- defaultValues,
- })
- )
- expect(baseElement).toBeTruthy()
- })
-
- it('should display create', () => {
- renderWithProviders(
- wrapWithReactHookForm( , {
- defaultValues,
- })
- )
- screen.getByText('Create')
- })
-
- it('should display update', () => {
- renderWithProviders(
- wrapWithReactHookForm( , {
- defaultValues,
- })
- )
- screen.getByText('Update')
- })
-
- it('should call onSubmit', async () => {
- const spy = jest.fn().mockImplementation((e) => e.preventDefault())
- props.onSubmit = spy
- const { userEvent } = renderWithProviders(
- wrapWithReactHookForm( , {
- defaultValues,
- })
- )
- const url = screen.getByLabelText('URL')
- fireEvent.change(url, { target: { value: 'https://test.com' } })
-
- const button = screen.getByText('Create')
-
- await userEvent.click(button)
-
- expect(props.onSubmit).toHaveBeenCalled()
- })
-})
diff --git a/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.tsx b/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.tsx
deleted file mode 100644
index b82d8299fb8..00000000000
--- a/libs/pages/settings/src/lib/ui/page-organization-webhooks/webhook-crud-modal/webhook-crud-modal.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-import {
- EnvironmentModeEnum,
- type OrganizationWebhookCreateRequest,
- OrganizationWebhookEventEnum,
- OrganizationWebhookKindEnum,
-} from 'qovery-typescript-axios'
-import { type FormEventHandler } from 'react'
-import { Controller, useFormContext } from 'react-hook-form'
-import { IconEnum } from '@qovery/shared/enums'
-import { Icon, InputSelect, InputTags, InputText, InputTextArea, ModalCrud } from '@qovery/shared/ui'
-
-export interface WebhookCrudModalProps {
- closeModal: () => void
- onSubmit: FormEventHandler
- isEdition?: boolean
- isLoading?: boolean
- hasExistingSecret?: boolean
- isEditDirty?: boolean
-}
-
-export function WebhookCrudModal(props: WebhookCrudModalProps) {
- const { closeModal, onSubmit, isEdition, isLoading, hasExistingSecret, isEditDirty } = props
- const { control } = useFormContext()
-
- return (
-
- General
-
- (
-
- )}
- />
-
- (
- ,
- },
- {
- label: 'Standard',
- value: OrganizationWebhookKindEnum.STANDARD,
- icon: ,
- },
- ]}
- onChange={field.onChange}
- value={field.value}
- label="Kind"
- error={error?.message}
- />
- )}
- />
-
- (
-
- )}
- />
-
- {!isEdition && (
- (
-
- )}
- />
- )}
-
- Event & filters
-
-
- (
-
- )}
- />
-
-
-
-
(
-
- )}
- />
-
- Webhook will be triggered only for projects whose names match or, if you're using a wildcard, start with one
- of the values from your list.
-
- Press Enter to add a new value.
-
-
-
-
- (
-
- )}
- />
-
-
- {isEditDirty && hasExistingSecret && (
- <>
-
- Confirm your secret
- (
-
- )}
- />
- >
- )}
-
- )
-}
-
-export default WebhookCrudModal