diff --git a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.stories.tsx b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.stories.tsx new file mode 100644 index 0000000000000..159a2d45b8e63 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.stories.tsx @@ -0,0 +1,96 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { spyOn, userEvent, within } from "@storybook/test"; +import { API } from "api/api"; +import { selectTemplatesByGroup } from "api/queries/notifications"; +import type { DeploymentValues } from "api/typesGenerated"; +import { MockNotificationTemplates } from "testHelpers/entities"; +import { NotificationEvents } from "./NotificationEvents"; +import { baseMeta } from "./storybookUtils"; + +const meta: Meta = { + title: "pages/DeploymentSettings/NotificationsPage/NotificationEvents", + component: NotificationEvents, + args: { + defaultMethod: "smtp", + availableMethods: ["smtp", "webhook"], + templatesByGroup: selectTemplatesByGroup(MockNotificationTemplates), + deploymentValues: baseMeta.parameters.deploymentValues, + }, + ...baseMeta, +}; + +export default meta; + +type Story = StoryObj; + +export const SMTPNotConfigured: Story = { + args: { + deploymentValues: { + notifications: { + webhook: { + endpoint: "https://example.com", + }, + email: { + smarthost: "", + }, + }, + } as DeploymentValues, + }, +}; + +export const WebhookNotConfigured: Story = { + args: { + deploymentValues: { + notifications: { + webhook: { + endpoint: "", + }, + email: { + smarthost: "smtp.example.com", + from: "bob@localhost", + hello: "localhost", + }, + }, + } as DeploymentValues, + }, +}; + +export const Toggle: Story = { + play: async ({ canvasElement }) => { + spyOn(API, "updateNotificationTemplateMethod").mockResolvedValue(); + const user = userEvent.setup(); + const canvas = within(canvasElement); + const tmpl = MockNotificationTemplates[4]; + const option = await canvas.findByText(tmpl.name); + const li = option.closest("li"); + if (!li) { + throw new Error("Could not find li"); + } + const toggleButton = within(li).getByRole("button", { + name: "Webhook", + }); + await user.click(toggleButton); + await within(document.body).findByText("Notification method updated"); + }, +}; + +export const ToggleError: Story = { + play: async ({ canvasElement }) => { + spyOn(API, "updateNotificationTemplateMethod").mockRejectedValue({}); + const user = userEvent.setup(); + const canvas = within(canvasElement); + const tmpl = MockNotificationTemplates[4]; + const option = await canvas.findByText(tmpl.name); + const li = option.closest("li"); + if (!li) { + throw new Error("Could not find li"); + } + const toggleButton = within(li).getByRole("button", { + name: "Webhook", + }); + await user.click(toggleButton); + await within(document.body).findByText( + "Failed to update notification method", + ); + }, +}; diff --git a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.tsx b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.tsx new file mode 100644 index 0000000000000..cabf7a24c3704 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationEvents.tsx @@ -0,0 +1,247 @@ +import type { Interpolation, Theme } from "@emotion/react"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import Divider from "@mui/material/Divider"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText"; +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import Tooltip from "@mui/material/Tooltip"; +import { getErrorMessage } from "api/errors"; +import { + type selectTemplatesByGroup, + updateNotificationTemplateMethod, +} from "api/queries/notifications"; +import type { DeploymentValues } from "api/typesGenerated"; +import { Alert } from "components/Alert/Alert"; +import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { Stack } from "components/Stack/Stack"; +import { + type NotificationMethod, + castNotificationMethod, + methodIcons, + methodLabels, +} from "modules/notifications/utils"; +import { type FC, Fragment } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { docs } from "utils/docs"; + +type NotificationEventsProps = { + defaultMethod: NotificationMethod; + availableMethods: NotificationMethod[]; + templatesByGroup: ReturnType; + deploymentValues: DeploymentValues; +}; + +export const NotificationEvents: FC = ({ + defaultMethod, + availableMethods, + templatesByGroup, + deploymentValues, +}) => { + // Webhook + const hasWebhookNotifications = Object.values(templatesByGroup) + .flat() + .some((t) => t.method === "webhook"); + const webhookValues = deploymentValues.notifications?.webhook ?? {}; + const isWebhookConfigured = requiredFieldsArePresent(webhookValues, [ + "endpoint", + ]); + + // SMTP + const hasSMTPNotifications = Object.values(templatesByGroup) + .flat() + .some((t) => t.method === "smtp"); + const smtpValues = deploymentValues.notifications?.email ?? {}; + const isSMTPConfigured = requiredFieldsArePresent(smtpValues, [ + "smarthost", + "from", + "hello", + ]); + + return ( + + {hasWebhookNotifications && !isWebhookConfigured && ( + + Read the docs + + } + > + Webhook notifications are enabled, but not properly configured. + + )} + + {hasSMTPNotifications && !isSMTPConfigured && ( + + Read the docs + + } + > + SMTP notifications are enabled but not properly configured. + + )} + + {Object.entries(templatesByGroup).map(([group, templates]) => ( + + + + + + + {templates.map((tpl, i) => { + const value = castNotificationMethod(tpl.method || defaultMethod); + const isLastItem = i === templates.length - 1; + + return ( + + + + + + {!isLastItem && } + + ); + })} + + + ))} + + ); +}; + +function requiredFieldsArePresent( + obj: Record, + fields: string[], +): boolean { + return fields.every((field) => Boolean(obj[field])); +} + +type MethodToggleGroupProps = { + templateId: string; + options: NotificationMethod[]; + value: NotificationMethod; +}; + +const MethodToggleGroup: FC = ({ + value, + options, + templateId, +}) => { + const queryClient = useQueryClient(); + const updateMethodMutation = useMutation( + updateNotificationTemplateMethod(templateId, queryClient), + ); + + return ( + { + try { + await updateMethodMutation.mutateAsync({ + method, + }); + displaySuccess("Notification method updated"); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to update notification method"), + ); + } + }} + > + {options.map((method) => { + const Icon = methodIcons[method]; + const label = methodLabels[method]; + return ( + + { + // Retain the value if the user clicks the same button, ensuring + // at least one value remains selected. + if (method === value) { + e.preventDefault(); + e.stopPropagation(); + return; + } + }} + > + + + + ); + })} + + ); +}; + +const styles = { + listHeader: (theme) => ({ + background: theme.palette.background.paper, + borderBottom: `1px solid ${theme.palette.divider}`, + }), + listItemText: { + [`& .${listItemTextClasses.primary}`]: { + fontSize: 14, + fontWeight: 500, + }, + [`& .${listItemTextClasses.secondary}`]: { + fontSize: 14, + }, + }, + toggleGroup: (theme) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: 4, + }), + toggleButton: (theme) => ({ + border: 0, + borderRadius: 4, + fontSize: 16, + padding: "4px 8px", + color: theme.palette.text.disabled, + + "&:hover": { + color: theme.palette.text.primary, + }, + + "& svg": { + fontSize: "inherit", + }, + }), +} as Record>; diff --git a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.stories.tsx b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.stories.tsx index b3d994c2faad7..79984c46dd46e 100644 --- a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.stories.tsx +++ b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.stories.tsx @@ -1,106 +1,54 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { spyOn, userEvent, within } from "@storybook/test"; -import { API } from "api/api"; +import { userEvent, within } from "@storybook/test"; import { notificationDispatchMethodsKey, systemNotificationTemplatesKey, } from "api/queries/notifications"; -import type { DeploymentValues, SerpentOption } from "api/typesGenerated"; import { MockNotificationMethodsResponse, MockNotificationTemplates, - MockUser, } from "testHelpers/entities"; -import { - withAuthProvider, - withDashboardProvider, - withDeploySettings, - withGlobalSnackbar, -} from "testHelpers/storybook"; import { NotificationsPage } from "./NotificationsPage"; +import { baseMeta } from "./storybookUtils"; const meta: Meta = { title: "pages/DeploymentSettings/NotificationsPage", component: NotificationsPage, - parameters: { - experiments: ["notifications"], - queries: [ - { key: systemNotificationTemplatesKey, data: MockNotificationTemplates }, - { - key: notificationDispatchMethodsKey, - data: MockNotificationMethodsResponse, - }, - ], - user: MockUser, - permissions: { viewDeploymentValues: true }, - deploymentOptions: mockNotificationOptions(), - deploymentValues: { - notifications: { - webhook: { - endpoint: "https://example.com", - }, - email: { - smarthost: "smtp.example.com", - }, - }, - } as DeploymentValues, - }, - decorators: [ - withGlobalSnackbar, - withAuthProvider, - withDashboardProvider, - withDeploySettings, - ], + ...baseMeta, }; export default meta; type Story = StoryObj; -export const Default: Story = {}; - -export const NoEmailSmarthost: Story = { +export const LoadingTemplates: Story = { parameters: { - deploymentValues: { - notifications: { - webhook: { - endpoint: "https://example.com", - }, - email: { - smarthost: "", - }, + queries: [ + { + key: systemNotificationTemplatesKey, + data: undefined, }, - } as DeploymentValues, + { + key: notificationDispatchMethodsKey, + data: MockNotificationMethodsResponse, + }, + ], }, }; -export const NoWebhookEndpoint: Story = { +export const LoadingDispatchMethods: Story = { parameters: { - deploymentValues: { - notifications: { - webhook: { - endpoint: "", - }, - email: { - smarthost: "smtp.example.com", - }, + queries: [ + { key: systemNotificationTemplatesKey, data: MockNotificationTemplates }, + { + key: notificationDispatchMethodsKey, + data: undefined, }, - } as DeploymentValues, + ], }, }; -export const Toggle: Story = { - play: async ({ canvasElement }) => { - spyOn(API, "updateNotificationTemplateMethod").mockResolvedValue(); - const user = userEvent.setup(); - const canvas = within(canvasElement); - const option = await canvas.findByText("Workspace Marked as Dormant"); - const toggleButton = within(option.closest("li")!).getByRole("button", { - name: "Webhook", - }); - await user.click(toggleButton); - }, -}; +export const Events: Story = {}; export const Settings: Story = { play: async ({ canvasElement }) => { @@ -110,170 +58,3 @@ export const Settings: Story = { await user.click(settingsTab); }, }; - -function mockNotificationOptions(): SerpentOption[] { - return [ - { - name: "Notifications: Dispatch Timeout", - description: - "How long to wait while a notification is being sent before giving up.", - flag: "notifications-dispatch-timeout", - env: "CODER_NOTIFICATIONS_DISPATCH_TIMEOUT", - yaml: "dispatchTimeout", - default: "1m0s", - value: 60000000000, - annotations: { - format_duration: "true", - }, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - value_source: "default", - }, - { - name: "Notifications: Fetch Interval", - description: "How often to query the database for queued notifications.", - flag: "notifications-fetch-interval", - env: "CODER_NOTIFICATIONS_FETCH_INTERVAL", - yaml: "fetchInterval", - default: "15s", - value: 15000000000, - annotations: { - format_duration: "true", - }, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - { - name: "Notifications: Lease Count", - description: - "How many notifications a notifier should lease per fetch interval.", - flag: "notifications-lease-count", - env: "CODER_NOTIFICATIONS_LEASE_COUNT", - yaml: "leaseCount", - default: "20", - value: 20, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - { - name: "Notifications: Lease Period", - description: - "How long a notifier should lease a message. This is effectively how long a notification is 'owned' by a notifier, and once this period expires it will be available for lease by another notifier. Leasing is important in order for multiple running notifiers to not pick the same messages to deliver concurrently. This lease period will only expire if a notifier shuts down ungracefully; a dispatch of the notification releases the lease.", - flag: "notifications-lease-period", - env: "CODER_NOTIFICATIONS_LEASE_PERIOD", - yaml: "leasePeriod", - default: "2m0s", - value: 120000000000, - annotations: { - format_duration: "true", - }, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - { - name: "Notifications: Max Send Attempts", - description: "The upper limit of attempts to send a notification.", - flag: "notifications-max-send-attempts", - env: "CODER_NOTIFICATIONS_MAX_SEND_ATTEMPTS", - yaml: "maxSendAttempts", - default: "5", - value: 5, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - value_source: "default", - }, - { - name: "Notifications: Method", - description: - "Which delivery method to use (available options: 'smtp', 'webhook').", - flag: "notifications-method", - env: "CODER_NOTIFICATIONS_METHOD", - yaml: "method", - default: "smtp", - value: "smtp", - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - value_source: "env", - }, - { - name: "Notifications: Retry Interval", - description: "The minimum time between retries.", - flag: "notifications-retry-interval", - env: "CODER_NOTIFICATIONS_RETRY_INTERVAL", - yaml: "retryInterval", - default: "5m0s", - value: 300000000000, - annotations: { - format_duration: "true", - }, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - { - name: "Notifications: Store Sync Buffer Size", - description: - "The notifications system buffers message updates in memory to ease pressure on the database. This option controls how many updates are kept in memory. The lower this value the lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the database. It is recommended to keep this option at its default value.", - flag: "notifications-store-sync-buffer-size", - env: "CODER_NOTIFICATIONS_STORE_SYNC_BUFFER_SIZE", - yaml: "storeSyncBufferSize", - default: "50", - value: 50, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - { - name: "Notifications: Store Sync Interval", - description: - "The notifications system buffers message updates in memory to ease pressure on the database. This option controls how often it synchronizes its state with the database. The shorter this value the lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the database. It is recommended to keep this option at its default value.", - flag: "notifications-store-sync-interval", - env: "CODER_NOTIFICATIONS_STORE_SYNC_INTERVAL", - yaml: "storeSyncInterval", - default: "2s", - value: 2000000000, - annotations: { - format_duration: "true", - }, - group: { - name: "Notifications", - yaml: "notifications", - description: "Configure how notifications are processed and delivered.", - }, - hidden: true, - value_source: "default", - }, - ]; -} diff --git a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.tsx index 150c62280ff22..c073792248072 100644 --- a/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/DeploySettingsPage/NotificationsPage/NotificationsPage.tsx @@ -1,98 +1,22 @@ import type { Interpolation, Theme } from "@emotion/react"; -import Button from "@mui/material/Button"; -import Card from "@mui/material/Card"; -import Divider from "@mui/material/Divider"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText"; -import ToggleButton from "@mui/material/ToggleButton"; -import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; -import Tooltip from "@mui/material/Tooltip"; import { notificationDispatchMethods, selectTemplatesByGroup, systemNotificationTemplates, - updateNotificationTemplateMethod, } from "api/queries/notifications"; -import type { DeploymentValues } from "api/typesGenerated"; -import { Alert } from "components/Alert/Alert"; -import { displaySuccess } from "components/GlobalSnackbar/utils"; import { Loader } from "components/Loader/Loader"; -import { Stack } from "components/Stack/Stack"; import { TabLink, Tabs, TabsList } from "components/Tabs/Tabs"; -import { - type NotificationMethod, - castNotificationMethod, - methodIcons, - methodLabels, -} from "modules/notifications/utils"; +import { castNotificationMethod } from "modules/notifications/utils"; import { Section } from "pages/UserSettingsPage/Section"; -import { type FC, Fragment } from "react"; +import type { FC } from "react"; import { Helmet } from "react-helmet-async"; -import { useMutation, useQueries, useQueryClient } from "react-query"; +import { useQueries } from "react-query"; import { useSearchParams } from "react-router-dom"; import { deploymentGroupHasParent } from "utils/deployOptions"; -import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { useDeploySettings } from "../DeploySettingsLayout"; import OptionsTable from "../OptionsTable"; - -type MethodToggleGroupProps = { - templateId: string; - options: NotificationMethod[]; - value: NotificationMethod; -}; - -const MethodToggleGroup: FC = ({ - value, - options, - templateId, -}) => { - const queryClient = useQueryClient(); - const updateMethodMutation = useMutation( - updateNotificationTemplateMethod(templateId, queryClient), - ); - - return ( - { - await updateMethodMutation.mutateAsync({ - method, - }); - displaySuccess("Notification method updated"); - }} - > - {options.map((method) => { - const Icon = methodIcons[method]; - const label = methodLabels[method]; - return ( - - { - // Retain the value if the user clicks the same button, ensuring - // at least one value remains selected. - if (method === value) { - e.preventDefault(); - e.stopPropagation(); - return; - } - }} - > - - - - ); - })} - - ); -}; +import { NotificationEvents } from "./NotificationEvents"; export const NotificationsPage: FC = () => { const [searchParams] = useSearchParams(); @@ -134,7 +58,7 @@ export const NotificationsPage: FC = () => {
{ready ? ( tab === "events" ? ( - { ); }; -type EventsViewProps = { - defaultMethod: NotificationMethod; - availableMethods: NotificationMethod[]; - templatesByGroup: ReturnType; - deploymentValues: DeploymentValues; -}; - -const EventsView: FC = ({ - defaultMethod, - availableMethods, - templatesByGroup, - deploymentValues, -}) => { - return ( - - {availableMethods.includes("smtp") && - deploymentValues.notifications?.webhook.endpoint === "" && ( - - Read the docs - - } - > - Webhook notifications are enabled, but no endpoint has been - configured. - - )} - - {availableMethods.includes("smtp") && - deploymentValues.notifications?.email.smarthost === "" && ( - - Read the docs - - } - > - SMTP notifications are enabled, but no smarthost has been - configured. - - )} - - {Object.entries(templatesByGroup).map(([group, templates]) => ( - - - - - - - {templates.map((tpl, i) => { - const value = castNotificationMethod(tpl.method || defaultMethod); - const isLastItem = i === templates.length - 1; - - return ( - - - - - - {!isLastItem && } - - ); - })} - - - ))} - - ); -}; - export default NotificationsPage; const styles = { content: { paddingTop: 24 }, - listHeader: (theme) => ({ - background: theme.palette.background.paper, - borderBottom: `1px solid ${theme.palette.divider}`, - }), - listItemText: { - [`& .${listItemTextClasses.primary}`]: { - fontSize: 14, - fontWeight: 500, - }, - [`& .${listItemTextClasses.secondary}`]: { - fontSize: 14, - }, - }, - toggleGroup: (theme) => ({ - border: `1px solid ${theme.palette.divider}`, - borderRadius: 4, - }), - toggleButton: (theme) => ({ - border: 0, - borderRadius: 4, - fontSize: 16, - padding: "4px 8px", - color: theme.palette.text.disabled, - - "&:hover": { - color: theme.palette.text.primary, - }, - - "& svg": { - fontSize: "inherit", - }, - }), } as Record>; diff --git a/site/src/pages/DeploySettingsPage/NotificationsPage/storybookUtils.ts b/site/src/pages/DeploySettingsPage/NotificationsPage/storybookUtils.ts new file mode 100644 index 0000000000000..c422adb56adb9 --- /dev/null +++ b/site/src/pages/DeploySettingsPage/NotificationsPage/storybookUtils.ts @@ -0,0 +1,218 @@ +import type { Meta } from "@storybook/react"; +import { + notificationDispatchMethodsKey, + systemNotificationTemplatesKey, +} from "api/queries/notifications"; +import type { DeploymentValues, SerpentOption } from "api/typesGenerated"; +import { + MockNotificationMethodsResponse, + MockNotificationTemplates, + MockUser, +} from "testHelpers/entities"; +import { + withAuthProvider, + withDashboardProvider, + withDeploySettings, + withGlobalSnackbar, +} from "testHelpers/storybook"; +import type { NotificationsPage } from "./NotificationsPage"; + +// Extracted from a real API response +export const mockNotificationsDeploymentOptions: SerpentOption[] = [ + { + name: "Notifications: Dispatch Timeout", + description: + "How long to wait while a notification is being sent before giving up.", + flag: "notifications-dispatch-timeout", + env: "CODER_NOTIFICATIONS_DISPATCH_TIMEOUT", + yaml: "dispatchTimeout", + default: "1m0s", + value: 60000000000, + annotations: { + format_duration: "true", + }, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + value_source: "default", + }, + { + name: "Notifications: Fetch Interval", + description: "How often to query the database for queued notifications.", + flag: "notifications-fetch-interval", + env: "CODER_NOTIFICATIONS_FETCH_INTERVAL", + yaml: "fetchInterval", + default: "15s", + value: 15000000000, + annotations: { + format_duration: "true", + }, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, + { + name: "Notifications: Lease Count", + description: + "How many notifications a notifier should lease per fetch interval.", + flag: "notifications-lease-count", + env: "CODER_NOTIFICATIONS_LEASE_COUNT", + yaml: "leaseCount", + default: "20", + value: 20, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, + { + name: "Notifications: Lease Period", + description: + "How long a notifier should lease a message. This is effectively how long a notification is 'owned' by a notifier, and once this period expires it will be available for lease by another notifier. Leasing is important in order for multiple running notifiers to not pick the same messages to deliver concurrently. This lease period will only expire if a notifier shuts down ungracefully; a dispatch of the notification releases the lease.", + flag: "notifications-lease-period", + env: "CODER_NOTIFICATIONS_LEASE_PERIOD", + yaml: "leasePeriod", + default: "2m0s", + value: 120000000000, + annotations: { + format_duration: "true", + }, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, + { + name: "Notifications: Max Send Attempts", + description: "The upper limit of attempts to send a notification.", + flag: "notifications-max-send-attempts", + env: "CODER_NOTIFICATIONS_MAX_SEND_ATTEMPTS", + yaml: "maxSendAttempts", + default: "5", + value: 5, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + value_source: "default", + }, + { + name: "Notifications: Method", + description: + "Which delivery method to use (available options: 'smtp', 'webhook').", + flag: "notifications-method", + env: "CODER_NOTIFICATIONS_METHOD", + yaml: "method", + default: "smtp", + value: "smtp", + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + value_source: "env", + }, + { + name: "Notifications: Retry Interval", + description: "The minimum time between retries.", + flag: "notifications-retry-interval", + env: "CODER_NOTIFICATIONS_RETRY_INTERVAL", + yaml: "retryInterval", + default: "5m0s", + value: 300000000000, + annotations: { + format_duration: "true", + }, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, + { + name: "Notifications: Store Sync Buffer Size", + description: + "The notifications system buffers message updates in memory to ease pressure on the database. This option controls how many updates are kept in memory. The lower this value the lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the database. It is recommended to keep this option at its default value.", + flag: "notifications-store-sync-buffer-size", + env: "CODER_NOTIFICATIONS_STORE_SYNC_BUFFER_SIZE", + yaml: "storeSyncBufferSize", + default: "50", + value: 50, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, + { + name: "Notifications: Store Sync Interval", + description: + "The notifications system buffers message updates in memory to ease pressure on the database. This option controls how often it synchronizes its state with the database. The shorter this value the lower the change of state inconsistency in a non-graceful shutdown - but it also increases load on the database. It is recommended to keep this option at its default value.", + flag: "notifications-store-sync-interval", + env: "CODER_NOTIFICATIONS_STORE_SYNC_INTERVAL", + yaml: "storeSyncInterval", + default: "2s", + value: 2000000000, + annotations: { + format_duration: "true", + }, + group: { + name: "Notifications", + yaml: "notifications", + description: "Configure how notifications are processed and delivered.", + }, + hidden: true, + value_source: "default", + }, +]; + +export const baseMeta = { + parameters: { + experiments: ["notifications"], + queries: [ + { key: systemNotificationTemplatesKey, data: MockNotificationTemplates }, + { + key: notificationDispatchMethodsKey, + data: MockNotificationMethodsResponse, + }, + ], + user: MockUser, + permissions: { viewDeploymentValues: true }, + deploymentOptions: mockNotificationsDeploymentOptions, + deploymentValues: { + notifications: { + webhook: { + endpoint: "https://example.com", + }, + email: { + smarthost: "smtp.example.com", + from: "bob@localhost", + hello: "localhost", + }, + }, + } as DeploymentValues, + }, + decorators: [ + withGlobalSnackbar, + withAuthProvider, + withDashboardProvider, + withDeploySettings, + ], +} satisfies Meta;