Repository: trento-project/web
Pull request #3393: Alerting Settings WebUI
Author: login: skrech
Labels: name: enhancement
description: New feature or request
name: elixir
description: Pull requests that update Elixir code
name: env
description: Create an ephimeral environment for the pr branch
Created: 2025-03-19T09:45:50Z | Merged: 2025-05-30T08:42:10Z

# Description

Work-stream for the front-end part of the Alerting Settings. 

Fixes [TRNT-2711](https://jira.suse.com/browse/TRNT-2711) [TRNT-3723](https://jira.suse.com/browse/TRNT-3723)


Diff:
diff --git a/assets/js/common/AbilitiesMultiSelect/AbilitiesMultiSelect.jsx b/assets/js/common/AbilitiesMultiSelect/AbilitiesMultiSelect.jsx
index a4b4044f76..b2a5b302b1 100644
--- a/assets/js/common/AbilitiesMultiSelect/AbilitiesMultiSelect.jsx
+++ b/assets/js/common/AbilitiesMultiSelect/AbilitiesMultiSelect.jsx
@@ -47,6 +47,7 @@ const groupedAbilities = [
       'all:api_key_settings',
       'all:suma_settings',
       'all:activity_logs_settings',
+      'all:alerting_settings',
     ],
   },
   {
diff --git a/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
new file mode 100644
index 0000000000..0b69bff2ff
--- /dev/null
+++ b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+    enforcedFromEnv = false,
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  const alertingFields = [
+    {
+      label: 'SMTP Server',
+      value: smtpServer,
+      ariaLabel: 'smtp-server',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'SMTP Port',
+      value: smtpPort,
+      ariaLabel: 'smtp-port',
+    },
+    {
+      label: 'SMTP User',
+      value: smtpUsername,
+      ariaLabel: 'smtp-username',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Password',
+      value: '•••••',
+      ariaLabel: 'smtp-password',
+    },
+    {
+      label: 'Alerting Sender',
+      value: senderEmail,
+      ariaLabel: 'alerting-sender',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Alerting Recipient',
+      value: recipientEmail,
+      ariaLabel: 'alerting-recipient',
+    },
+    {
+      label: 'Send Email Alerts',
+      value: alertingEnabled ? 'Enabled' : 'Disabled',
+      ariaLabel: 'alerting-enabled',
+    },
+  ];
+
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Tooltip
+              isEnabled={enforcedFromEnv}
+              content={ENFORCED_FROM_ENV_MESSAGE}
+              place="bottom"
+            >
+              <Button
+                type="primary-white-fit"
+                size="small"
+                aria-label="alerting-edit-button"
+                onClick={onEditClick}
+                disabled={enforcedFromEnv}
+              >
+                Edit Settings
+              </Button>
+            </Tooltip>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 gap-y-3 mt-5 items-center">
+        {alertingFields.map(({ label, value, ariaLabel, addClasses }) => (
+          <AlertingSettingsRow
+            key={ariaLabel}
+            label={label}
+            value={value}
+            ariaLabel={ariaLabel}
+            addClasses={addClasses}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+function AlertingSettingsRow({ label, value, ariaLabel, addClasses = '' }) {
+  return (
+    <>
+      <div className="font-bold">{label}</div>
+      <div
+        aria-label={ariaLabel}
+        className={`col-span-2 text-gray-500 ${addClasses}`}
+      >
+        {value}
+      </div>
+    </>
+  );
+}
diff --git a/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.stories.jsx b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.stories.jsx
new file mode 100644
index 0000000000..04d8fb9753
--- /dev/null
+++ b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.stories.jsx
@@ -0,0 +1,51 @@
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+export default {
+  title: 'Components/AlertingSettingsConfig',
+  component: AlertingSettingsConfig,
+  argTypes: {
+    settings: {
+      description: 'Current alerting settings values',
+      control: {
+        type: 'object',
+      },
+    },
+
+    userAbilities: {
+      description: 'Abilities of the current user',
+      control: {
+        type: 'array',
+      },
+    },
+
+    onEditClick: {
+      description: 'Callback that would run on edit button being clicked',
+      control: { type: 'function' },
+    },
+  },
+};
+
+export const Default = {};
+
+export const WithFilledInValues = {
+  args: {
+    settings: alertingSettingsFactory.build(),
+  },
+};
+
+export const WithEditButtonEnabledWhenEnoughPermissions = {
+  args: {
+    userAbilities: [{ name: 'all', resource: 'all' }],
+  },
+};
+
+export const WithEditButtonDisabledWhenEnforcedFromEnv = {
+  args: {
+    settings: {
+      enforcedFromEnv: true,
+    },
+    userAbilities: [{ name: 'all', resource: 'all' }],
+  },
+};
diff --git a/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
new file mode 100644
index 0000000000..493260b8ee
--- /dev/null
+++ b/assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arguments', () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    render(<AlertingSettingsConfig settings={alertingSettings} />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(smtpServer);
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+      smtpPort.toString()
+    );
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+      smtpUsername
+    );
+    expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      senderEmail
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      recipientEmail
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Enabled'
+    );
+  });
+
+  it.each`
+    case                        | abilities
+    ${'not set'}                | ${undefined}
+    ${'empty list'}             | ${[]}
+    ${'non-relevant abilities'} | ${[{ name: 'all', resource: 'some_resource' }]}
+  `(
+    'forbids editing when user has not sufficient abilities ($case)',
+    async ({ abilities }) => {
+      const onEditClick = jest.fn();
+      const user = userEvent.setup();
+
+      render(
+        <AlertingSettingsConfig
+          onEditClick={onEditClick}
+          userAbilities={abilities}
+        />
+      );
+
+      const editButton = screen.getByLabelText('alerting-edit-button');
+      expect(editButton).toBeDisabled();
+
+      await user.click(editButton);
+      expect(onEditClick).not.toHaveBeenCalled();
+
+      await user.hover(editButton);
+      expect(
+        screen.queryByText('You are not authorized for this action')
+      ).toBeVisible();
+    }
+  );
+
+  it.each`
+    case                       | abilities
+    ${'admin'}                 | ${adminUser}
+    ${'all:alerting_settings'} | ${[{ name: 'all', resource: 'alerting_settings' }]}
+  `(
+    'allows editing when user has sufficient abilities ($case) and calls correct handler',
+    async ({ abilities }) => {
+      const onEditClick = jest.fn();
+      const user = userEvent.setup();
+
+      render(
+        <AlertingSettingsConfig
+          onEditClick={onEditClick}
+          userAbilities={abilities}
+        />
+      );
+
+      expect(screen.getByLabelText('alerting-edit-button')).toBeEnabled();
+
+      await user.click(screen.getByLabelText('alerting-edit-button'));
+      expect(onEditClick).toHaveBeenCalledTimes(1);
+    }
+  );
+
+  it('disables editing when settings are enforced from application env', async () => {
+    const onEditClick = jest.fn();
+    const user = userEvent.setup();
+
+    render(
+      <AlertingSettingsConfig
+        settings={alertingSettingsFactory.build({ enforcedFromEnv: true })}
+        onEditClick={onEditClick}
+        userAbilities={adminUser}
+      />
+    );
+
+    const editButton = screen.getByLabelText('alerting-edit-button');
+    expect(editButton).toBeDisabled();
+
+    await user.click(editButton);
+    expect(onEditClick).not.toHaveBeenCalled();
+
+    await user.hover(editButton);
+    expect(screen.queryByText(ENFORCED_FROM_ENV_MESSAGE)).toBeVisible();
+  });
+});
diff --git a/assets/js/common/AlertingSettingsConfig/index.js b/assets/js/common/AlertingSettingsConfig/index.js
new file mode 100644
index 0000000000..4626eb94ae
--- /dev/null
+++ b/assets/js/common/AlertingSettingsConfig/index.js
@@ -0,0 +1,3 @@
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+export default AlertingSettingsConfig;
diff --git a/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
new file mode 100644
index 0000000000..dc5ae8567d
--- /dev/null
+++ b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
@@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  const onSubmit = (e) => {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer: smtpServer || null,
+      smtpPort: Number(smtpPort) || null,
+      smtpUsername: smtpUsername || null,
+      senderEmail: senderEmail || null,
+      recipientEmail: recipientEmail || null,
+      ...(editingPassword && { smtpPassword: smtpPassword || null }),
+    };
+    onSave(settingsPayload);
+  };
+
+  return (
+    <Modal title="Enter Alerting Settings" open={open} onClose={onCancel}>
+      <form onSubmit={onSubmit}>
+        <div className="grid grid-cols-6 my-5 gap-6">
+          <Label className="col-span-2" htmlFor="alerting-enabled-input">
+            Send Email Alerts
+          </Label>
+          <div className="col-span-4">
+            <Switch
+              id="alerting-enabled-input"
+              name="alerting-enabled-input"
+              selected={alertingEnabled}
+              onChange={(value) => {
+                setAlertingEnabled(value);
+              }}
+            />
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-server-input" required>
+            SMTP Server
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-server-input"
+              name="smtp-server-input"
+              value={smtpServer}
+              placeholder="Enter a URL"
+              error={hasError('smtp_server', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpServer(value);
+              }}
+            />
+            {hasError('smtp_server', errors) && (
+              <p
+                aria-label="smtp-server-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_server', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-port-input" required>
+            SMTP Port
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-port-input"
+              name="smtp-port-input"
+              value={smtpPort}
+              placeholder={587}
+              error={hasError('smtp_port', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpPort(value);
+              }}
+            />
+            {hasError('smtp_port', errors) && (
+              <p
+                aria-label="smtp-port-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_port', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-username-input" required>
+            SMTP Username
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-username-input"
+              name="smtp-username-input"
+              value={smtpUsername}
+              placeholder="Enter SMTP Username"
+              error={hasError('smtp_username', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpUsername(value);
+              }}
+            />
+            {hasError('smtp_username', errors) && (
+              <p
+                aria-label="smtp-username-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_username', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            id="smtp-password-label"
+            htmlFor="smtp-password-input"
+            required
+          >
+            SMTP Password
+          </Label>
+          {editingPassword ? (
+            <div className="col-span-4">
+              <Password
+                aria-labelledby="smtp-password-label"
+                name="smtp-password-input"
+                placeholder="Enter SMTP Password"
+                value={smtpPassword}
+                error={hasError('smtp_password', errors)}
+                onChange={({ target: { value } }) => {
+                  setSmtpPassword(value);
+                }}
+              />
+              {hasError('smtp_password', errors) && (
+                <p
+                  aria-label="smtp-password-input-error"
+                  role="alert"
+                  className="text-red-500 mt-1"
+                >
+                  {capitalize(getError('smtp_password', errors))}
+                </p>
+              )}
+            </div>
+          ) : (
+            <div className="col-span-4 border border-gray-200 p-5 rounded-md">
+              <p
+                aria-labelledby="smtp-password-label"
+                className="inline align-sub leading-10"
+              >
+                •••••
+              </p>
+              <Button
+                className="float-right"
+                type="danger"
+                onClick={() => setEditingPassword(true)}
+              >
+                Remove
+              </Button>
+            </div>
+          )}
+
+          <Label className="col-span-2" htmlFor="sender-email-input" required>
+            Alert Sender
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="sender-email-input"
+              name="sender-email-input"
+              value={senderEmail}
+              placeholder="Enter an email address"
+              error={hasError('sender_email', errors)}
+              onChange={({ target: { value } }) => {
+                setSenderEmail(value);
+              }}
+            />
+            {hasError('sender_email', errors) && (
+              <p
+                aria-label="sender-email-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('sender_email', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            htmlFor="recipient-email-input"
+            required
+          >
+            Alert Recipient
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="recipient-email-input"
+              name="recipient-email-input"
+              value={recipientEmail}
+              placeholder="Enter an email address"
+              error={hasError('recipient_email', errors)}
+              onChange={({ target: { value } }) => {
+                setRecipientEmail(value);
+              }}
+            />
+            {hasError('recipient_email', errors) && (
+              <p
+                aria-label="recipient-email-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('recipient_email', errors))}
+              </p>
+            )}
+          </div>
+        </div>
+
+        <div className="flex flex-row w-80 space-x-2">
+          <Button asSubmit disabled={loading}>
+            Save Settings
+          </Button>
+          <Button type="primary-white" onClick={onCancel}>
+            Cancel
+          </Button>
+        </div>
+      </form>
+    </Modal>
+  );
+}
diff --git a/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.stories.jsx b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.stories.jsx
new file mode 100644
index 0000000000..52c35a9cf2
--- /dev/null
+++ b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.stories.jsx
@@ -0,0 +1,143 @@
+import { userEvent, within } from '@storybook/test';
+
+import AlertingSettingsModal from './AlertingSettingsModal';
+
+const validSettings = {
+  alertingEnabled: true,
+  smtpServer: 'smtp.testdomain.com',
+  smtpPort: 587,
+  smtpUsername: 'testuser',
+  senderEmail: 'admin@testdomain.com',
+  recipientEmail: 'trentousers@testdomain.com',
+};
+
+export default {
+  title: 'Components/AlertingSettingsModal',
+  component: AlertingSettingsModal,
+  parameters: {
+    docs: {
+      story: {
+        inline: false,
+        iframeHeight: 500,
+      },
+    },
+  },
+  argTypes: {
+    previousSettings: {
+      description: 'Alerting settings that could be configured',
+      control: {
+        type: 'object',
+      },
+    },
+
+    errors: {
+      description: 'Errors from failed submission',
+      control: {
+        type: 'object',
+      },
+    },
+
+    open: {
+      description: 'Whether the dialog is open or not',
+      type: 'boolean',
+      control: {
+        type: 'boolean',
+      },
+    },
+
+    loading: {
+      description: 'Whether submission is in progress',
+      control: {
+        type: 'boolean',
+      },
+    },
+
+    onSave: {
+      description: 'Callback that would run on submit',
+      control: { type: 'function' },
+    },
+
+    onCancel: {
+      description: 'Callback that would run on Cancel button clicked',
+      control: { type: 'function' },
+    },
+  },
+};
+
+export const Default = {
+  args: {
+    open: true,
+  },
+};
+
+export const WithPreviousSettings = {
+  args: {
+    previousSettings: validSettings,
+    open: true,
+  },
+};
+
+export const WithPreviousSettingsAndEditablePassword = {
+  args: {
+    previousSettings: validSettings,
+    open: true,
+  },
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement.parentElement);
+    await userEvent.click(canvas.getByRole('button', { name: 'Remove' }));
+  },
+};
+
+export const WithErrors = {
+  args: {
+    previousSettings: {
+      alertingEnabled: true,
+      smtpServer: 'localhost',
+      smtpPort: 70000,
+      smtpUsername: 'inv4!d name',
+      senderEmail: 'wrong_email.com',
+      recipientEmail: 'mailey.com',
+    },
+    errors: [
+      {
+        detail: 'error from backend',
+        source: { pointer: '/smtp_server' },
+        title: 'Invalid value',
+      },
+      {
+        detail: 'error from backend',
+        source: { pointer: '/smtp_port' },
+        title: 'Invalid value',
+      },
+      {
+        detail: 'error from backend',
+        source: { pointer: '/smtp_username' },
+        title: 'Invalid value',
+      },
+      {
+        detail: 'error from backend',
+        source: { pointer: '/smtp_password' },
+        title: 'Invalid value',
+      },
+      {
+        detail: 'error from backend',
+        source: { pointer: '/sender_email' },
+        title: 'Invalid value',
+      },
+      {
+        detail: 'error from backend',
+        source: { pointer: '/recipient_email' },
+        title: 'Invalid value',
+      },
+    ],
+    open: true,
+  },
+};
+
+export const Loading = {
+  args: {
+    previousSettings: validSettings,
+    open: true,
+    loading: true,
+  },
+};
diff --git a/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.test.jsx b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.test.jsx
new file mode 100644
index 0000000000..3a1b372ded
--- /dev/null
+++ b/assets/js/common/AlertingSettingsModal/AlertingSettingsModal.test.jsx
@@ -0,0 +1,223 @@
+import React from 'react';
+import { faker } from '@faker-js/faker';
+import { act, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { capitalize } from 'lodash';
+
+import {
+  alertingSettingsFactory,
+  alertingSettingsSaveRequestFactory,
+} from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsModal from './AlertingSettingsModal';
+
+describe('AlertingSettingsModal', () => {
+  it('renders correctly when opened with no arguments passed', async () => {
+    await act(() => {
+      render(<AlertingSettingsModal open />);
+    });
+
+    const alertingEnabled = screen.getByRole('switch', {
+      name: 'Send Email Alerts',
+    });
+    expect(alertingEnabled).not.toBeChecked();
+
+    const smtpServer = screen.getByRole('textbox', { name: 'SMTP Server *' });
+    expect(smtpServer).toHaveAttribute('placeholder', 'Enter a URL');
+    expect(smtpServer).toHaveTextContent('');
+
+    const smtpPort = screen.getByRole('textbox', { name: 'SMTP Port *' });
+    expect(smtpPort).toHaveAttribute('placeholder', '587');
+    expect(smtpPort).toHaveTextContent('');
+
+    const smtpUsername = screen.getByRole('textbox', {
+      name: 'SMTP Username *',
+    });
+    expect(smtpUsername).toHaveAttribute('placeholder', 'Enter SMTP Username');
+    expect(smtpUsername).toHaveTextContent('');
+
+    const smtpPassword = screen.getByLabelText(/^SMTP Password/, {
+      selector: ':placeholder-shown',
+    });
+    expect(smtpPassword).toHaveAttribute('placeholder', 'Enter SMTP Password');
+
+    const emailSender = screen.getByRole('textbox', { name: 'Alert Sender *' });
+    expect(emailSender).toHaveAttribute(
+      'placeholder',
+      'Enter an email address'
+    );
+    expect(emailSender).toHaveTextContent('');
+
+    const emailRecipient = screen.getByRole('textbox', {
+      name: 'Alert Recipient *',
+    });
+    expect(emailRecipient).toHaveAttribute(
+      'placeholder',
+      'Enter an email address'
+    );
+    expect(emailRecipient).toHaveTextContent('');
+  });
+
+  it('renders correctly when opened with supplied settings arg', async () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    await act(() => {
+      render(
+        <AlertingSettingsModal previousSettings={alertingSettings} open />
+      );
+    });
+
+    expect(
+      screen.getByRole('switch', { name: 'Send Email Alerts' })
+    ).toBeChecked();
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Server *' })
+    ).toHaveDisplayValue(smtpServer);
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Port *' })
+    ).toHaveDisplayValue(smtpPort.toString());
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Username *' })
+    ).toHaveDisplayValue(smtpUsername);
+    expect(screen.getByLabelText(/^SMTP Password/)).toHaveTextContent('•••••');
+    expect(
+      screen.getByRole('textbox', { name: 'Alert Sender *' })
+    ).toHaveDisplayValue(senderEmail);
+    expect(
+      screen.getByRole('textbox', { name: 'Alert Recipient *' })
+    ).toHaveDisplayValue(recipientEmail);
+  });
+
+  it('calls onSave handler correctly when supplying fresh settings', async () => {
+    const {
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      smtpPassword,
+      senderEmail,
+      recipientEmail,
+    } = alertingSettingsSaveRequestFactory.build();
+
+    const user = userEvent.setup();
+    const onSave = jest.fn();
+
+    render(<AlertingSettingsModal open onSave={onSave} />);
+
+    await user.click(screen.getByRole('switch', 'Email Alerts'));
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Server *' }),
+      smtpServer
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Port *' }),
+      smtpPort.toString()
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Username *' }),
+      smtpUsername
+    );
+    await user.type(screen.getByLabelText(/^SMTP Password/), smtpPassword);
+    await user.type(
+      screen.getByRole('textbox', { name: 'Alert Sender *' }),
+      senderEmail
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'Alert Recipient *' }),
+      recipientEmail
+    );
+    await user.click(screen.getByRole('button', { name: 'Save Settings' }));
+
+    expect(onSave).toBeCalledWith({
+      alertingEnabled: true,
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      smtpPassword,
+      senderEmail,
+      recipientEmail,
+    });
+  });
+
+  it('does not supply password to onSave handler by default when previous settings', async () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    const user = userEvent.setup();
+    const onSave = jest.fn();
+
+    render(
+      <AlertingSettingsModal
+        previousSettings={alertingSettings}
+        open
+        onSave={onSave}
+      />
+    );
+    await user.click(screen.getByRole('button', { name: 'Save Settings' }));
+
+    expect(onSave).toBeCalledWith({
+      alertingEnabled: true,
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      // No `smtpPassword'
+      senderEmail,
+      recipientEmail,
+    });
+  });
+
+  it('renders errors correctly', async () => {
+    const constructErrorMessage = (pointer, errorDetails) => ({
+      detail: errorDetails,
+      source: { pointer: `/${pointer}` },
+      title: 'Invalid value',
+    });
+
+    const smtpServerError = faker.lorem.words();
+    const smtpPortError = faker.lorem.words();
+    const smtpUsernameError = faker.lorem.words();
+    const smtpPasswordError = faker.lorem.words();
+    const senderEmailError = faker.lorem.words();
+    const recipientEmailError = faker.lorem.words();
+
+    const user = userEvent.setup();
+
+    render(
+      <AlertingSettingsModal
+        open
+        previousSettings={alertingSettingsFactory.build()}
+        errors={[
+          constructErrorMessage('smtp_server', smtpServerError),
+          constructErrorMessage('smtp_port', smtpPortError),
+          constructErrorMessage('smtp_username', smtpUsernameError),
+          constructErrorMessage('smtp_password', smtpPasswordError),
+          constructErrorMessage('sender_email', senderEmailError),
+          constructErrorMessage('recipient_email', recipientEmailError),
+        ]}
+      />
+    );
+    await user.click(screen.getByRole('button', { name: 'Remove' }));
+
+    expect(
+      screen.getByRole('alert', { name: 'smtp-server-input-error' })
+    ).toHaveTextContent(capitalize(smtpServerError));
+    expect(
+      screen.getByRole('alert', { name: 'smtp-port-input-error' })
+    ).toHaveTextContent(capitalize(smtpPortError));
+    expect(
+      screen.getByRole('alert', { name: 'smtp-username-input-error' })
+    ).toHaveTextContent(capitalize(smtpUsernameError));
+    expect(
+      screen.getByRole('alert', { name: 'smtp-password-input-error' })
+    ).toHaveTextContent(capitalize(smtpPasswordError));
+    expect(
+      screen.getByRole('alert', { name: 'sender-email-input-error' })
+    ).toHaveTextContent(capitalize(senderEmailError));
+    expect(
+      screen.getByRole('alert', { name: 'recipient-email-input-error' })
+    ).toHaveTextContent(capitalize(recipientEmailError));
+  });
+});
diff --git a/assets/js/common/AlertingSettingsModal/index.js b/assets/js/common/AlertingSettingsModal/index.js
new file mode 100644
index 0000000000..91d14b0e41
--- /dev/null
+++ b/assets/js/common/AlertingSettingsModal/index.js
@@ -0,0 +1,3 @@
+import AlertingSettingsModal from './AlertingSettingsModal';
+
+export default AlertingSettingsModal;
diff --git a/assets/js/common/Button/Button.jsx b/assets/js/common/Button/Button.jsx
index 6b124bfaa9..4d7b8db74d 100644
--- a/assets/js/common/Button/Button.jsx
+++ b/assets/js/common/Button/Button.jsx
@@ -35,7 +35,15 @@ const getButtonClasses = (type) => {
   }
 };
 
-function Button({ children, className, type, size, disabled, ...props }) {
+function Button({
+  children,
+  className,
+  type,
+  size,
+  disabled,
+  asSubmit,
+  ...props
+}) {
   const buttonClasses = classNames(
     getButtonClasses(type),
     getSizeClasses(size),
@@ -45,7 +53,7 @@ function Button({ children, className, type, size, disabled, ...props }) {
 
   return (
     <button
-      type="button"
+      type={asSubmit ? 'submit' : 'button'}
       className={buttonClasses}
       disabled={disabled}
       {...props}
diff --git a/assets/js/common/SettingsLoader/SettingsLoader.jsx b/assets/js/common/SettingsLoader/SettingsLoader.jsx
index 61c1655f7e..f78174941e 100644
--- a/assets/js/common/SettingsLoader/SettingsLoader.jsx
+++ b/assets/js/common/SettingsLoader/SettingsLoader.jsx
@@ -38,7 +38,7 @@ export const calculateStatus = (isLoading, isError) => {
 
 /**
  * Wrap a section of the settings page and show a placeholder
- * until data is not available.
+ * until data is available.
  *
  * @param props.sectionName (optional) Name of the section, default: empty string
  * @param props.status Whether the section is LOADING, ERROR or READY,
diff --git a/assets/js/common/Switch/Switch.jsx b/assets/js/common/Switch/Switch.jsx
index 7ed68ff725..b06dd344b4 100644
--- a/assets/js/common/Switch/Switch.jsx
+++ b/assets/js/common/Switch/Switch.jsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
 
 import { Switch as HeadlessSwitch } from '@headlessui/react';
 
-function Switch({ onChange, selected, disabled }) {
+function Switch({ onChange, selected, disabled, ...props }) {
   return (
     <HeadlessSwitch.Group as="div" className="flex items-center">
       <HeadlessSwitch
@@ -20,6 +20,7 @@ function Switch({ onChange, selected, disabled }) {
         )}
         disabled={disabled}
         onChange={onChange}
+        {...props}
       >
         <span
           aria-hidden="true"
diff --git a/assets/js/lib/api/alertingSettings.js b/assets/js/lib/api/alertingSettings.js
new file mode 100644
index 0000000000..b272039f26
--- /dev/null
+++ b/assets/js/lib/api/alertingSettings.js
@@ -0,0 +1,11 @@
+import { networkClient } from '@lib/network';
+
+const alertingPath = '/settings/alerting';
+
+export const getSettings = () => networkClient.get(alertingPath);
+
+export const saveSettings = (settings) =>
+  networkClient.post(alertingPath, settings);
+
+export const updateSettings = (settings) =>
+  networkClient.patch(alertingPath, settings);
diff --git a/assets/js/lib/test-utils/factories/alertingSettings.js b/assets/js/lib/test-utils/factories/alertingSettings.js
new file mode 100644
index 0000000000..f5baf17e7b
--- /dev/null
+++ b/assets/js/lib/test-utils/factories/alertingSettings.js
@@ -0,0 +1,40 @@
+import { faker } from '@faker-js/faker';
+import { Factory } from 'fishery';
+
+const baseAlertingSettingsFactory = Factory.define(() => ({
+  alertingEnabled: true,
+  smtpServer: faker.internet.domainName(),
+  smtpPort: faker.number.int({ min: 1, max: 65535 }),
+  smtpUsername: faker.internet.username(),
+  senderEmail: faker.internet.email(),
+  recipientEmail: faker.internet.email(),
+}));
+
+export const alertingSettingsFactory = baseAlertingSettingsFactory.params({
+  enforcedFromEnv: false,
+});
+
+export const alertingSettingsToApiData = (alertingSettings) => ({
+  enabled: alertingSettings.alertingEnabled,
+  smtp_server: alertingSettings.smtpServer,
+  smtp_port: alertingSettings.smtpPort,
+  smtp_username: alertingSettings.smtpUsername,
+  sender_email: alertingSettings.senderEmail,
+  recipient_email: alertingSettings.recipientEmail,
+  enforced_from_env: alertingSettings.enforcedFromEnv,
+});
+
+export const alertingSettingsSaveRequestFactory =
+  baseAlertingSettingsFactory.params({
+    smtpPassword: faker.animal.dog(),
+  });
+
+export const alertingSettingsSaveRequestToApiData = (saveAlertingSettings) => ({
+  enabled: saveAlertingSettings.alertingEnabled,
+  smtp_server: saveAlertingSettings.smtpServer,
+  smtp_port: saveAlertingSettings.smtpPort,
+  smtp_username: saveAlertingSettings.smtpUsername,
+  smtp_password: saveAlertingSettings.smtpPassword,
+  sender_email: saveAlertingSettings.senderEmail,
+  recipient_email: saveAlertingSettings.recipientEmail,
+});
diff --git a/assets/js/lib/test-utils/index.jsx b/assets/js/lib/test-utils/index.jsx
index 1cb8f6ffc9..0855e3e64c 100644
--- a/assets/js/lib/test-utils/index.jsx
+++ b/assets/js/lib/test-utils/index.jsx
@@ -74,8 +74,18 @@ export const hookWrapperWithState = (initialState = defaultInitialState) => {
 export const renderWithRouter = (ui, { route = '/' } = {}) => {
   window.history.pushState({}, 'Test page', route);
 
+  function Router({ children }) {
+    return (
+      <BrowserRouter
+        future={{ v7_relativeSplatPath: true, v7_startTransition: true }}
+      >
+        {children}
+      </BrowserRouter>
+    );
+  }
+
   return {
-    ...render(ui, { wrapper: BrowserRouter }),
+    ...render(ui, { wrapper: Router }),
   };
 };
 
diff --git a/assets/js/pages/SettingsPage/SettingsPage.jsx b/assets/js/pages/SettingsPage/SettingsPage.jsx
index 0646221b3a..2da2187663 100644
--- a/assets/js/pages/SettingsPage/SettingsPage.jsx
+++ b/assets/js/pages/SettingsPage/SettingsPage.jsx
@@ -33,9 +33,13 @@ import {
   getActivityLogsSettingsErrors,
 } from '@state/selectors/activityLogsSettings';
 
+import AlertingSettingsConfig from '@common/AlertingSettingsConfig';
+import AlertingSettingsModal from '@common/AlertingSettingsModal';
+
 import {
   useApiKeySettings,
   useSuseManagerSettings,
+  useAlertingSettings,
 } from '@pages/SettingsPage/hooks';
 
 const apiKeySettingsPermittedFor = ['all:api_key_settings'];
@@ -113,6 +117,23 @@ function SettingsPage() {
 
   const hasApiKey = Boolean(apiKey);
 
+  const {
+    settings: alertingSettings,
+    fetchLoading: alertingFetchLoading,
+    fetchError: alertingFetchError,
+    submitLoading: alertingSubmitLoading,
+    submitErrors: alertingSubmitErrors,
+    fetch: fetchAlertingSettings,
+    submit: submitAlertingSettings,
+    clearSubmitErrors: clearAlertingSubmitErrors,
+  } = useAlertingSettings();
+
+  const [alertingSettingsModalOpen, setAlertingSettingsModalOpen] =
+    useState(false);
+  useEffect(() => {
+    setAlertingSettingsModalOpen(false);
+  }, [alertingSettings]);
+
   return (
     <>
       <PageHeader className="font-bold">Settings</PageHeader>
@@ -299,36 +320,70 @@ function SettingsPage() {
       </section>
 
       <section>
-        <SettingsLoader
-          sectionName="Activity Logs"
-          status={calculateSettingsLoaderStatus(
-            activityLogsSettingsLoading,
-            activityLogsSettingsNetworkError
-          )}
-          onRetry={() => dispatch(fetchActivityLogsSettings())}
-        >
-          <ActivityLogsConfig
-            userAbilities={abilities}
-            retentionTime={activityLogsSettings.retention_time}
-            onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Activity Logs"
+            status={calculateSettingsLoaderStatus(
+              activityLogsSettingsLoading,
+              activityLogsSettingsNetworkError
+            )}
+            onRetry={() => dispatch(fetchActivityLogsSettings())}
+          >
+            <ActivityLogsConfig
+              userAbilities={abilities}
+              retentionTime={activityLogsSettings.retention_time}
+              onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+            />
+          </SettingsLoader>
+          <ActivityLogsSettingsModal
+            key={`${JSON.stringify(
+              activityLogsSettings
+            )}-${editingActivityLogsSettings}`}
+            open={editingActivityLogsSettings}
+            errors={activityLogsValidationErrors}
+            loading={activityLogsSettingsLoading}
+            initialRetentionTime={activityLogsSettings.retention_time}
+            onSave={(payload) => {
+              dispatch(updateActivityLogsSettings(payload));
+            }}
+            onCancel={() => {
+              dispatch(setActivityLogsSettingsErrors([]));
+              dispatch(setEditingActivityLogsSettings(false));
+            }}
           />
-        </SettingsLoader>
-        <ActivityLogsSettingsModal
-          key={`${JSON.stringify(
-            activityLogsSettings
-          )}-${editingActivityLogsSettings}`}
-          open={editingActivityLogsSettings}
-          errors={activityLogsValidationErrors}
-          loading={activityLogsSettingsLoading}
-          initialRetentionTime={activityLogsSettings.retention_time}
-          onSave={(payload) => {
-            dispatch(updateActivityLogsSettings(payload));
-          }}
-          onCancel={() => {
-            dispatch(setActivityLogsSettingsErrors([]));
-            dispatch(setEditingActivityLogsSettings(false));
-          }}
-        />
+        </div>
+      </section>
+
+      <section>
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Alerting"
+            status={calculateSettingsLoaderStatus(
+              alertingFetchLoading,
+              alertingFetchError
+            )}
+            onRetry={() => fetchAlertingSettings()}
+          >
+            <AlertingSettingsConfig
+              settings={alertingSettings}
+              userAbilities={abilities}
+              onEditClick={() => {
+                clearAlertingSubmitErrors();
+                setAlertingSettingsModalOpen(true);
+              }}
+            />
+          </SettingsLoader>
+
+          <AlertingSettingsModal
+            key={`alertingmodal-${Object.values(alertingSettings).join('-')}-${alertingSettingsModalOpen}`}
+            previousSettings={alertingSettings}
+            errors={alertingSubmitErrors}
+            open={alertingSettingsModalOpen}
+            loading={alertingSubmitLoading}
+            onSave={submitAlertingSettings}
+            onCancel={() => setAlertingSettingsModalOpen(false)}
+          />
+        </div>
       </section>
     </>
   );
diff --git a/assets/js/pages/SettingsPage/SettingsPage.test.jsx b/assets/js/pages/SettingsPage/SettingsPage.test.jsx
index eb3b480a98..d0847e0837 100644
--- a/assets/js/pages/SettingsPage/SettingsPage.test.jsx
+++ b/assets/js/pages/SettingsPage/SettingsPage.test.jsx
@@ -15,18 +15,15 @@ import { networkClient } from '@lib/network';
 import MockAdapter from 'axios-mock-adapter';
 
 import SettingsPage from './SettingsPage';
+import {
+  alertingSettingsFactory,
+  alertingSettingsToApiData,
+} from '../../lib/test-utils/factories/alertingSettings';
 
 const axiosMock = new MockAdapter(networkClient);
 
 const defaultInitialState = {
   ...defaultInitialStateBase,
-  softwareUpdatesSettings: {
-    settings: {
-      url: undefined,
-      username: undefined,
-      ca_uploaded_at: undefined,
-    },
-  },
   activityLogsSettings: {
     settings: {
       retention_time: { value: 1, unit: 'day' },
@@ -85,7 +82,9 @@ describe('Settings Page', () => {
         ...defaultInitialState,
       });
 
-      renderWithRouter(StatefulSettings);
+      await act(async () => {
+        renderWithRouter(StatefulSettings);
+      });
 
       expect(
         screen.getByText('Loading SUSE Manager Settings...')
@@ -109,10 +108,13 @@ describe('Settings Page', () => {
       expect(screen.getByText('CA Certificate')).toBeVisible();
       expect(screen.getByText('-')).toBeVisible();
 
-      expect(screen.getByText('Username')).toBeVisible();
-      expect(screen.getByText('Password')).toBeVisible();
+      const sumaUsername = screen.getByLabelText('suma-username');
+      expect(sumaUsername).toBeVisible();
+      expect(sumaUsername).toHaveTextContent('.....');
 
-      expect(screen.queryAllByText('.....')).toHaveLength(2);
+      const sumaPassword = screen.getByLabelText('suma-password');
+      expect(sumaPassword).toBeVisible();
+      expect(sumaPassword).toHaveTextContent('.....');
     });
 
     it('should render SUSE Manager Config Section with configured settings', async () => {
@@ -138,11 +140,13 @@ describe('Settings Page', () => {
         screen.getByText(format(ca_uploaded_at, "'Uploaded:' dd MMM y"))
       ).toBeVisible();
 
-      expect(screen.getByText('Username')).toBeVisible();
-      expect(screen.getByText(username)).toBeVisible();
+      const sumaUsername = screen.getByLabelText('suma-username');
+      expect(sumaUsername).toBeVisible();
+      expect(sumaUsername).toHaveTextContent(username);
 
-      expect(screen.getByText('Password')).toBeVisible();
-      expect(screen.getByText('•••••')).toBeVisible();
+      const sumaPassword = screen.getByLabelText('suma-password');
+      expect(sumaPassword).toBeVisible();
+      expect(sumaPassword).toHaveTextContent('•••••');
     });
   });
 
@@ -253,4 +257,96 @@ describe('Settings Page', () => {
       ).toBeVisible();
     });
   });
+
+  describe('Alerting Section', () => {
+    it('renders a loading box while fetching settings', async () => {
+      axiosMock.onGet('/api/v1/settings/alerting').reply(
+        (_) =>
+          new Promise((resolve) => {
+            setTimeout(() => resolve([200, {}]), 5000);
+          })
+      );
+
+      const [StatefulSettings] = withState(<SettingsPage />, {
+        ...defaultInitialState,
+      });
+
+      await act(async () => {
+        renderWithRouter(StatefulSettings);
+      });
+
+      expect(screen.getByText('Loading Alerting Settings...')).toBeVisible();
+    });
+
+    it('renders a default Alerting Config Section when initial fetch returns 404', async () => {
+      const [StatefulSettings] = withState(<SettingsPage />, {
+        ...defaultInitialState,
+      });
+
+      axiosMock.onGet('/api/v1/settings/alerting').reply(404, {});
+
+      await act(async () => {
+        renderWithRouter(StatefulSettings);
+      });
+
+      expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+        'https://...'
+      );
+      expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+      expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+      expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+      expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+        '...@...'
+      );
+      expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+        '...@...'
+      );
+      expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+        'Disabled'
+      );
+    });
+
+    it('renders Alerting Config Section on successful initial fetch', async () => {
+      const alertingSettings = alertingSettingsFactory.build();
+      const {
+        smtpServer,
+        smtpPort,
+        smtpUsername,
+        senderEmail,
+        recipientEmail,
+      } = alertingSettings;
+
+      const [StatefulSettings] = withState(<SettingsPage />, {
+        ...defaultInitialState,
+      });
+
+      axiosMock
+        .onGet('/api/v1/settings/alerting')
+        .reply(200, alertingSettingsToApiData(alertingSettings));
+
+      await act(async () => {
+        renderWithRouter(StatefulSettings);
+      });
+
+      expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+        smtpServer
+      );
+      expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+        smtpPort.toString()
+      );
+      expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+        smtpUsername
+      );
+      expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+      expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+        senderEmail
+      );
+      expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+        recipientEmail
+      );
+      expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+        'Enabled'
+      );
+    });
+  });
 });
diff --git a/assets/js/pages/SettingsPage/hooks.js b/assets/js/pages/SettingsPage/hooks.js
index 24583b11b4..fd70d8db76 100644
--- a/assets/js/pages/SettingsPage/hooks.js
+++ b/assets/js/pages/SettingsPage/hooks.js
@@ -1,8 +1,11 @@
 import { useState, useEffect } from 'react';
-import { logError } from '@lib/log';
+import { isEmpty, omitBy, isNil } from 'lodash';
 import { useDispatch } from 'react-redux';
+
 import { dismissNotification, notify } from '@state/notifications';
 import { API_KEY_EXPIRATION_NOTIFICATION_ID } from '@state/sagas/settings';
+
+import { logError } from '@lib/log';
 import { get, patch } from '@lib/network';
 import {
   getSettings,
@@ -11,6 +14,11 @@ import {
   clearSettings,
   testConnection,
 } from '@lib/api/suseManagerSettings';
+import {
+  getSettings as getAlertingSettings,
+  saveSettings as saveAlertingSettings,
+  updateSettings as updateAlertingSettings,
+} from '@lib/api/alertingSettings';
 
 export const useSuseManagerSettings = () => {
   const dispatch = useDispatch();
@@ -162,3 +170,92 @@ export const useApiKeySettings = () => {
     fetchApiKeySettings,
   };
 };
+
+export const useAlertingSettings = () => {
+  const [settings, setSettings] = useState({});
+  const [fetchLoading, setFetchLoading] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+  const [submitErrors, setSubmitErrors] = useState([]);
+  const [submitLoading, setSubmitLoading] = useState(false);
+
+  function fromApiSettings(data) {
+    return {
+      alertingEnabled: data.enabled,
+      smtpServer: data.smtp_server,
+      smtpPort: data.smtp_port,
+      smtpUsername: data.smtp_username,
+      senderEmail: data.sender_email,
+      recipientEmail: data.recipient_email,
+      enforcedFromEnv: data.enforced_from_env,
+    };
+  }
+
+  function toApiSettings(newSettings) {
+    const data = {
+      enabled: newSettings.alertingEnabled,
+      smtp_server: newSettings.smtpServer,
+      smtp_port: newSettings.smtpPort,
+      smtp_username: newSettings.smtpUsername,
+      smtp_password: newSettings.smtpPassword,
+      sender_email: newSettings.senderEmail,
+      recipient_email: newSettings.recipientEmail,
+    };
+
+    return omitBy(data, isNil);
+  }
+
+  function clearSubmitErrors() {
+    setSubmitErrors([]);
+  }
+
+  async function fetch() {
+    setFetchLoading(true);
+    setFetchError(false);
+
+    try {
+      const { data } = await getAlertingSettings();
+      setSettings(fromApiSettings(data));
+    } catch ({ response: { status } }) {
+      setSettings({});
+      if (status !== 404) setFetchError(true);
+    } finally {
+      setFetchLoading(false);
+    }
+  }
+
+  async function submit(newSettings) {
+    setSubmitLoading(true);
+    clearSubmitErrors([]);
+    const action = isEmpty(settings)
+      ? saveAlertingSettings
+      : updateAlertingSettings;
+
+    try {
+      const { data } = await action(toApiSettings(newSettings));
+      setSettings(fromApiSettings(data));
+    } catch ({
+      response: {
+        data: { errors },
+      },
+    }) {
+      setSubmitErrors(errors);
+    } finally {
+      setSubmitLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    fetch();
+  }, []);
+
+  return {
+    settings,
+    fetchLoading,
+    fetchError,
+    submitLoading,
+    submitErrors,
+    fetch,
+    submit,
+    clearSubmitErrors,
+  };
+};
diff --git a/assets/js/pages/SettingsPage/hooks.test.jsx b/assets/js/pages/SettingsPage/hooks.test.jsx
index 6557131c82..d9184d6f88 100644
--- a/assets/js/pages/SettingsPage/hooks.test.jsx
+++ b/assets/js/pages/SettingsPage/hooks.test.jsx
@@ -1,12 +1,20 @@
 import {
+  useAlertingSettings,
   useApiKeySettings,
   useSuseManagerSettings,
 } from '@pages/SettingsPage/hooks';
 import { networkClient } from '@lib/network';
 import MockAdapter from 'axios-mock-adapter';
+import { flow, pick, set } from 'lodash/fp';
 import { faker } from '@faker-js/faker';
 import { act, renderHook } from '@testing-library/react';
 import { hookWrapperWithState } from '@lib/test-utils';
+import {
+  alertingSettingsFactory,
+  alertingSettingsToApiData,
+  alertingSettingsSaveRequestFactory,
+  alertingSettingsSaveRequestToApiData,
+} from '@lib/test-utils/factories/alertingSettings';
 
 const axiosMock = new MockAdapter(networkClient);
 
@@ -319,3 +327,138 @@ describe('useApiKeySettings', () => {
     expect(hookResult.current.apiKey).toBe('new_api_key');
   });
 });
+
+describe('useAlertingSettings', () => {
+  const saveRequestAlertingSettings =
+    alertingSettingsSaveRequestFactory.build();
+  const saveRequestAlertingData = alertingSettingsSaveRequestToApiData(
+    saveRequestAlertingSettings
+  );
+
+  const savedAlertingSettings = flow(
+    pick([
+      'alertingEnabled',
+      'smtpServer',
+      'smtpPort',
+      'smtpUsername',
+      'senderEmail',
+      'recipientEmail',
+    ]),
+    set('enforcedFromEnv', false)
+  )(saveRequestAlertingSettings);
+
+  const savedAlertingData = flow(
+    pick([
+      'enabled',
+      'smtp_server',
+      'smtp_port',
+      'smtp_username',
+      'sender_email',
+      'recipient_email',
+    ]),
+    set('enforced_from_env', false)
+  )(saveRequestAlertingData);
+
+  afterEach(() => {
+    axiosMock.reset();
+  });
+
+  describe('failed with non-404 error on initial fetch and', () => {
+    it('sets empty settings and activates error flag', async () => {
+      axiosMock.onGet('/api/v1/settings/alerting').reply(422);
+
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+
+      expect(result.current.settings).toEqual({});
+      expect(result.current.fetchError).toEqual(true);
+    });
+  });
+
+  describe('failed with 404 error on initial fetch and', () => {
+    it("sets empty settings but doesn't activate error flag", async () => {
+      axiosMock.onGet('/api/v1/settings/alerting').reply(404);
+
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+
+      expect(result.current.settings).toEqual({});
+      expect(result.current.fetchError).toEqual(false);
+    });
+
+    it('sends POST request with correct body when submit callback is called', async () => {
+      axiosMock.onGet('/api/v1/settings/alerting').replyOnce(404);
+
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+      const submitFn = result.current.submit;
+      axiosMock
+        .onPost('/api/v1/settings/alerting', saveRequestAlertingData)
+        .reply(200, savedAlertingData);
+
+      await act(() => {
+        submitFn(saveRequestAlertingSettings);
+      });
+
+      expect(result.current.settings).toEqual(savedAlertingSettings);
+      expect(result.current.submitErrors).toEqual([]);
+    });
+  });
+
+  describe('succeeds on initial fetch and', () => {
+    const fetchedAlertingSettings = alertingSettingsFactory.build();
+
+    beforeEach(() => {
+      axiosMock
+        .onGet('/api/v1/settings/alerting')
+        .reply(200, alertingSettingsToApiData(fetchedAlertingSettings));
+    });
+
+    it('correctly sets alerting settings', async () => {
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+
+      expect(result.current.settings).toEqual(fetchedAlertingSettings);
+    });
+
+    it('sends PATCH request with correct body when submit callback is called', async () => {
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+      const submitFn = result.current.submit;
+      axiosMock
+        .onPatch('/api/v1/settings/alerting', saveRequestAlertingData)
+        .reply(200, savedAlertingData);
+
+      await act(() => {
+        submitFn(saveRequestAlertingSettings);
+      });
+
+      expect(result.current.settings).toEqual(savedAlertingSettings);
+      expect(result.current.submitErrors).toEqual([]);
+    });
+
+    it('sets errors on HTTP failure', async () => {
+      const { result } = await act(() =>
+        renderHook(() => useAlertingSettings())
+      );
+      const submitFn = result.current.submit;
+      const errors = [
+        { title: 'Test error', detail: "It's really a test error" },
+      ];
+      axiosMock
+        .onPatch('/api/v1/settings/alerting', saveRequestAlertingData)
+        .reply(409, { errors });
+
+      await act(() => {
+        submitFn(saveRequestAlertingSettings);
+      });
+
+      expect(result.current.submitErrors).toEqual(errors);
+    });
+  });
+});
diff --git a/lib/trento_web/openapi/v1/schema/platform.ex b/lib/trento_web/openapi/v1/schema/platform.ex
index cfe32b28c4..33479ddaa8 100644
--- a/lib/trento_web/openapi/v1/schema/platform.ex
+++ b/lib/trento_web/openapi/v1/schema/platform.ex
@@ -316,14 +316,10 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Platform do
         type: :object,
         properties: %{
           enabled: %Schema{type: :boolean},
-          sender_email: %Schema{type: :string},
-          recipient_email: %Schema{type: :string},
+          sender_email: %Schema{type: :string, format: :email},
+          recipient_email: %Schema{type: :string, format: :email},
           smtp_server: %Schema{type: :string},
-          smtp_port: %Schema{
-            title: "Port",
-            anyOf: [%Schema{type: :integer}, %Schema{type: :string}],
-            example: 587
-          },
+          smtp_port: %Schema{type: :integer, example: 587},
           smtp_username: %Schema{type: :string},
           smtp_password: %Schema{type: :string, format: :password}
         },
@@ -351,14 +347,10 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Platform do
         type: :object,
         properties: %{
           enabled: %Schema{type: :boolean},
-          sender_email: %Schema{type: :string},
-          recipient_email: %Schema{type: :string},
+          sender_email: %Schema{type: :string, format: :email},
+          recipient_email: %Schema{type: :string, format: :email},
           smtp_server: %Schema{type: :string},
-          smtp_port: %Schema{
-            title: "Port",
-            anyOf: [%Schema{type: :integer}, %Schema{type: :string}],
-            example: 587
-          },
+          smtp_port: %Schema{type: :integer, example: 587},
           smtp_username: %Schema{type: :string},
           smtp_password: %Schema{type: :string, format: :password}
         }


Reviews:
user: login: nelsonkopliku
body: Hey @skrech, good job!
Before going deeper into the code I noticed a few visual/behavioral improvement opportunities.

<details>

<summary>Edit Settings button misalignment</summary>

We might align the `Edit Settings` button with the ones in the other sections
![misalignment](https://github.com/user-attachments/assets/9292bbfc-6fe4-44ba-8ec9-6b00a6e2b49f)

</details>

<details>

<summary>Missing feedback when saving/updating settings fails</summary>

When incorrect data is provided and submitted, there is not visual feedback for the user. This might make it unclear what is going on.

Options we have are:
- a generic `An error occurred`
- a per field validation error message (see what happens in SMLM settings)

Additionally there's the `NaN` apperaring when non numbers are provided as the port. We might leave the API validate that and show the related validation error message

https://github.com/user-attachments/assets/905cad66-4cb9-4088-a00e-3211f1214395

</details>

<details>

<summary>Stale modal state</summary>

The changes made to the edit form that are not successfully applied (in case the API fails or the modal is closed before submission) are retained between edit modal openings.
For a better UX I believe we need to consider this cases and possibly clean up the state.

https://github.com/user-attachments/assets/58f8dabb-d484-4b18-a330-b0bd7107db5e

</details>

Happy to answer any question.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T14:54:50Z
user: login: nelsonkopliku
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T07:41:37Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:49:59Z
user: login: EMaksy
body: Hey  @skrech  nice work, left some ideas, tell me what you think :)
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T11:35:37Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T07:13:16Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T07:14:05Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T07:29:33Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T07:46:34Z
user: login: EMaksy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T08:05:54Z
user: login: nelsonkopliku
body: Hey @skrech very nice!

The main thing we need to address are the missing tests. For the rest it does not seem that much is missing.

There's something we might want to improve with openapi validation like making the port an integer only and adding a `format: :email` to email.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T08:07:01Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T10:40:05Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:54:12Z
user: login: EMaksy
body: You did a great progress!
Just some more nitpicks and ideas. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T10:15:32Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:39:52Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:41:43Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:41:53Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T12:44:50Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:24:59Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:48:00Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T13:55:43Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T14:03:55Z
user: login: nelsonkopliku
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T14:27:06Z
user: login: nelsonkopliku
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T14:29:39Z
user: login: nelsonkopliku
body: Hey @skrech, well done!

There are still a few rough edges that might be polished especially related to user input to request transformation and related validation, but I believe we can push this forward and possibly address them later in smaller, laser focused PRs.

EDIT: don't worry about the failing **API bc check (V1)** step in the pipeline, we'll force merge

✉️ 🔔 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T16:14:26Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T09:07:18Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T12:57:53Z
user: login: skrech
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T12:59:10Z
user: login: nelsonkopliku
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T08:40:21Z
user: login: nelsonkopliku
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-30T08:40:40Z

Review comments:
diff_hunk: @@ -0,0 +1,104 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: nelsonkopliku
body: **suggestion:** for consistency with other settings and if not differently agreed, we might want to have the alerting settings permission grouped with the other settings permissions in `all:settings`
https://github.com/trento-project/web/blob/14c370e1851f48cac6eba7c2d158315d25f93098/assets/js/common/AbilitiesMultiSelect/AbilitiesMultiSelect.jsx#L44
created_at: 2025-05-14T15:02:41Z
author_association: MEMBER
diff_hunk: @@ -74,8 +74,17 @@ export const hookWrapperWithState = (initialState = defaultInitialState) => {
 export const renderWithRouter = (ui, { route = '/' } = {}) => {
   window.history.pushState({}, 'Test page', route);
 
+  const Router = ({ children }) => {
+    return (
+      <BrowserRouter
+        future={{ v7_relativeSplatPath: true, v7_startTransition: true }}
path: assets/js/lib/test-utils/index.jsx
user: login: nelsonkopliku
body: Thanks, I noticed the logs were complaining 🙈 
created_at: 2025-05-14T15:13:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { faker } from '@faker-js/faker';
+import * as lodash from 'lodash';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arugments', () => {
+    const alertingEnabled = true;
+    const smtpServer = faker.internet.url();
+    const smtpPort = faker.number.int({ max: 60000 });
+    const smtpUsername = faker.animal.cow();
+    const senderEmail = faker.internet.email();
+    const recipientEmail = faker.internet.email();
+
+    render(
+      <AlertingSettingsConfig
+        settings={{
+          alertingEnabled,
+          smtpServer,
+          smtpPort,
+          smtpUsername,
+          senderEmail,
+          recipientEmail,
+        }}
+      />
+    );
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(smtpServer);
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+      smtpPort.toString()
+    );
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+      smtpUsername
+    );
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      senderEmail
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      recipientEmail
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Enabled'
+    );
+  });
+
+  it('forbids editing when called with default arguments', async () => {
+    var spy = jest.spyOn(lodash, 'noop');
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: What made you prefer a spy over a `const mockOnEditClick = jest.fn();` here?
created_at: 2025-05-14T15:18:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { faker } from '@faker-js/faker';
+import * as lodash from 'lodash';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arugments', () => {
+    const alertingEnabled = true;
+    const smtpServer = faker.internet.url();
+    const smtpPort = faker.number.int({ max: 60000 });
+    const smtpUsername = faker.animal.cow();
+    const senderEmail = faker.internet.email();
+    const recipientEmail = faker.internet.email();
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: question: what about having a dedicated factory for alerting settings?
created_at: 2025-05-15T07:39:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { faker } from '@faker-js/faker';
+import * as lodash from 'lodash';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arugments', () => {
+    const alertingEnabled = true;
+    const smtpServer = faker.internet.url();
+    const smtpPort = faker.number.int({ max: 60000 });
+    const smtpUsername = faker.animal.cow();
+    const senderEmail = faker.internet.email();
+    const recipientEmail = faker.internet.email();
+
+    render(
+      <AlertingSettingsConfig
+        settings={{
+          alertingEnabled,
+          smtpServer,
+          smtpPort,
+          smtpUsername,
+          senderEmail,
+          recipientEmail,
+        }}
+      />
+    );
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(smtpServer);
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+      smtpPort.toString()
+    );
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+      smtpUsername
+    );
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      senderEmail
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      recipientEmail
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Enabled'
+    );
+  });
+
+  it('forbids editing when called with default arguments', async () => {
+    var spy = jest.spyOn(lodash, 'noop');
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: skrech
body: Looking at the test, I don't know why I chose a `spy`, will change it to a simple func mock. I suspect the first implementation was different and the mocked operation was needed to execute.
created_at: 2025-05-15T07:49:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,103 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Button
+              type="primary-white-fit"
+              size="small"
+              aria-label="alerting-edit-button"
+              onClick={onEditClick}
+            >
+              Edit Settings
+            </Button>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 mt-5 items-center">
+        <div className="font-bold mb-3">SMTP Server</div>
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: EMaksy
body: Hey what about reducing repetition with a small reusable component? 

This pattern is repeated a lot: 
```
<div className="font-bold mb-3">{Label}</div>
<div className="col-span-2 text-gray-500 mb-3">{VARIABLE}</div>
```

Maybe we can create a component something in this direction....

```
const AlertSetingRow = ({ label, value, ariaLabel }) => (
  <>
    <div className="font-bold mb-3">{label}</div>
    <div
      aria-label={ariaLabel}
      className="col-span-2 text-gray-500 mb-3 truncate pr-12"
    >
      {value}
    </div>
  </>
);
```

Then you could reuse
``<AlertSetingRow label="SMTP Server" value={smtpServer} ariaLabel="smtp-server" />``


Also consider using a const 

```
const fields = [
  { label: 'SMTP Server', value: smtpServer, ariaLabel: 'smtp-server' },
  <<All other values>>
  {
    label: 'Send Email Alerts',
    value: alertingEnabled ? 'Enabled' : 'Disabled',
    ariaLabel: 'alerting-enabled',
  },
];
```

You could use map to render everything inside this config. 

Soomething like this ? 
```
<div className="grid grid-cols-6 mt-5 items-center">
  {fields.map(({ label, value, ariaLabel }) => (
    <AlertSettingRow
      key={ariaLabel}
      label={label}
      value={value}
      ariaLabel={ariaLabel}
    />
  ))}
</div>
```
With this you would have less code, easy to maintain and extend :) 


What do you think ? 
created_at: 2025-05-19T11:14:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { faker } from '@faker-js/faker';
+import * as lodash from 'lodash';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arugments', () => {
+    const alertingEnabled = true;
+    const smtpServer = faker.internet.url();
+    const smtpPort = faker.number.int({ max: 60000 });
+    const smtpUsername = faker.animal.cow();
+    const senderEmail = faker.internet.email();
+    const recipientEmail = faker.internet.email();
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: EMaksy
body: +1 to add a factory
created_at: 2025-05-19T11:14:49Z
author_association: MEMBER
diff_hunk: @@ -299,36 +319,70 @@ function SettingsPage() {
       </section>
 
       <section>
-        <SettingsLoader
-          sectionName="Activity Logs"
-          status={calculateSettingsLoaderStatus(
-            activityLogsSettingsLoading,
-            activityLogsSettingsNetworkError
-          )}
-          onRetry={() => dispatch(fetchActivityLogsSettings())}
-        >
-          <ActivityLogsConfig
-            userAbilities={abilities}
-            retentionTime={activityLogsSettings.retention_time}
-            onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Activity Logs"
+            status={calculateSettingsLoaderStatus(
+              activityLogsSettingsLoading,
+              activityLogsSettingsNetworkError
+            )}
+            onRetry={() => dispatch(fetchActivityLogsSettings())}
+          >
+            <ActivityLogsConfig
+              userAbilities={abilities}
+              retentionTime={activityLogsSettings.retention_time}
+              onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+            />
+          </SettingsLoader>
+          <ActivityLogsSettingsModal
+            key={`${JSON.stringify(
+              activityLogsSettings
+            )}-${editingActivityLogsSettings}`}
+            open={editingActivityLogsSettings}
+            errors={activityLogsValidationErrors}
+            loading={activityLogsSettingsLoading}
+            initialRetentionTime={activityLogsSettings.retention_time}
+            onSave={(payload) => {
+              dispatch(updateActivityLogsSettings(payload));
+            }}
+            onCancel={() => {
+              dispatch(setActivityLogsSettingsErrors([]));
+              dispatch(setEditingActivityLogsSettings(false));
+            }}
           />
-        </SettingsLoader>
-        <ActivityLogsSettingsModal
-          key={`${JSON.stringify(
-            activityLogsSettings
-          )}-${editingActivityLogsSettings}`}
-          open={editingActivityLogsSettings}
-          errors={activityLogsValidationErrors}
-          loading={activityLogsSettingsLoading}
-          initialRetentionTime={activityLogsSettings.retention_time}
-          onSave={(payload) => {
-            dispatch(updateActivityLogsSettings(payload));
-          }}
-          onCancel={() => {
-            dispatch(setActivityLogsSettingsErrors([]));
-            dispatch(setEditingActivityLogsSettings(false));
-          }}
-        />
+        </div>
+      </section>
+
+      <section>
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Alerting Settings"
+            status={calculateSettingsLoaderStatus(
+              alertingFetchLoading,
+              alertingFetchError
+            )}
+            onRetry={() => fetchAlertingSettings()}
+          >
+            <AlertingSettingsConfig
+              settings={alertingSettings}
+              userAbilities={abilities}
+              onEditClick={() => {
+                clearAlertingSubmitErrors()
+                setAlertingSettingsModalOpen(true);
+              }}
+            />
+          </SettingsLoader>
+
+          <AlertingSettingsModal
+            key={`alertingmodal-${Object.values(alertingSettings).join("-")}-${alertingSettingsModalOpen}`}
path: assets/js/pages/SettingsPage/SettingsPage.jsx
user: login: EMaksy
body: Maybe there is a better alternative then using Object.values as key ? It may cause unnecessary remounts, because of the unpredictable ordering and also requires some performance for the key calculation.
Something more like https://github.com/trento-project/web/blob/7dce2f71f5598fedfb61938786cffc1e86a7348a/assets/js/pages/SettingsPage/SettingsPage.jsx#L298. 

Also the readability is harder like this.

What do you think  ? 
created_at: 2025-05-19T11:29:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,104 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: skrech
body: Addressed. 
created_at: 2025-05-20T07:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,109 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { faker } from '@faker-js/faker';
+import * as lodash from 'lodash';
+
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arugments', () => {
+    const alertingEnabled = true;
+    const smtpServer = faker.internet.url();
+    const smtpPort = faker.number.int({ max: 60000 });
+    const smtpUsername = faker.animal.cow();
+    const senderEmail = faker.internet.email();
+    const recipientEmail = faker.internet.email();
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: skrech
body: Created factories and put the in use in the AlertingSettingsConfig and AlertingSettingsModal tests.
created_at: 2025-05-20T07:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,103 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Button
+              type="primary-white-fit"
+              size="small"
+              aria-label="alerting-edit-button"
+              onClick={onEditClick}
+            >
+              Edit Settings
+            </Button>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 mt-5 items-center">
+        <div className="font-bold mb-3">SMTP Server</div>
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: skrech
body: Thanks Eugen, looks like a good idea, I'm only concerned  about the CSS values because I know that they differ in some cases (last rows, if I recall correctly). Anyway, I think I know how to handle that. :) 
created_at: 2025-05-20T07:29:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -299,36 +319,70 @@ function SettingsPage() {
       </section>
 
       <section>
-        <SettingsLoader
-          sectionName="Activity Logs"
-          status={calculateSettingsLoaderStatus(
-            activityLogsSettingsLoading,
-            activityLogsSettingsNetworkError
-          )}
-          onRetry={() => dispatch(fetchActivityLogsSettings())}
-        >
-          <ActivityLogsConfig
-            userAbilities={abilities}
-            retentionTime={activityLogsSettings.retention_time}
-            onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Activity Logs"
+            status={calculateSettingsLoaderStatus(
+              activityLogsSettingsLoading,
+              activityLogsSettingsNetworkError
+            )}
+            onRetry={() => dispatch(fetchActivityLogsSettings())}
+          >
+            <ActivityLogsConfig
+              userAbilities={abilities}
+              retentionTime={activityLogsSettings.retention_time}
+              onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+            />
+          </SettingsLoader>
+          <ActivityLogsSettingsModal
+            key={`${JSON.stringify(
+              activityLogsSettings
+            )}-${editingActivityLogsSettings}`}
+            open={editingActivityLogsSettings}
+            errors={activityLogsValidationErrors}
+            loading={activityLogsSettingsLoading}
+            initialRetentionTime={activityLogsSettings.retention_time}
+            onSave={(payload) => {
+              dispatch(updateActivityLogsSettings(payload));
+            }}
+            onCancel={() => {
+              dispatch(setActivityLogsSettingsErrors([]));
+              dispatch(setEditingActivityLogsSettings(false));
+            }}
           />
-        </SettingsLoader>
-        <ActivityLogsSettingsModal
-          key={`${JSON.stringify(
-            activityLogsSettings
-          )}-${editingActivityLogsSettings}`}
-          open={editingActivityLogsSettings}
-          errors={activityLogsValidationErrors}
-          loading={activityLogsSettingsLoading}
-          initialRetentionTime={activityLogsSettings.retention_time}
-          onSave={(payload) => {
-            dispatch(updateActivityLogsSettings(payload));
-          }}
-          onCancel={() => {
-            dispatch(setActivityLogsSettingsErrors([]));
-            dispatch(setEditingActivityLogsSettings(false));
-          }}
-        />
+        </div>
+      </section>
+
+      <section>
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Alerting Settings"
+            status={calculateSettingsLoaderStatus(
+              alertingFetchLoading,
+              alertingFetchError
+            )}
+            onRetry={() => fetchAlertingSettings()}
+          >
+            <AlertingSettingsConfig
+              settings={alertingSettings}
+              userAbilities={abilities}
+              onEditClick={() => {
+                clearAlertingSubmitErrors()
+                setAlertingSettingsModalOpen(true);
+              }}
+            />
+          </SettingsLoader>
+
+          <AlertingSettingsModal
+            key={`alertingmodal-${Object.values(alertingSettings).join("-")}-${alertingSettingsModalOpen}`}
path: assets/js/pages/SettingsPage/SettingsPage.jsx
user: login: skrech
body: Hmm... You mean to enumerate all the alerting settings fields (which are 7, and +1 for modality) here? 

I decided to use some dynamism here especially  to help on the readability front. 

As to the unnecessary re-mounts -- I don't think they are expected. I'm far from JS expert, but usually, in other languages, map keys are not guaranteed to be in order but should be consistent/`not change` once they are put into the map. 


created_at: 2025-05-20T07:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,103 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Button
+              type="primary-white-fit"
+              size="small"
+              aria-label="alerting-edit-button"
+              onClick={onEditClick}
+            >
+              Edit Settings
+            </Button>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 mt-5 items-center">
+        <div className="font-bold mb-3">SMTP Server</div>
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: EMaksy
body: You also could pass css as prop and give even more control :) 
The repeated css could be a default value, and you pass only if it differs or how ever you like. 
I'm sure you will find a good solution :) 
created_at: 2025-05-20T08:05:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: Is this still necessary without spies?
created_at: 2025-05-21T07:46:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: **nitpick:** password missing 🙈 
created_at: 2025-05-21T07:47:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
+  });
+
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arguments', () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    render(<AlertingSettingsConfig settings={alertingSettings} />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(smtpServer);
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+      smtpPort.toString()
+    );
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+      smtpUsername
+    );
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      senderEmail
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      recipientEmail
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Enabled'
+    );
+  });
+
+  it('forbids editing when called with default arguments', async () => {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: **suggestion:** we could rephrase to highlight what we want to test, that is not being able to perform an action when the user does not have sufficient abilities.
```suggestion
  it('forbids editing when user has not sufficient abilities', async () => {
```

Additionally this could be 
```js
it.each``
```
to cover different scenarios like empty abilities or not matching ones.

created_at: 2025-05-21T07:51:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,11 @@
+import { networkClient } from '@lib/network';
+
+const alerting_uri = '/settings/alerting';
path: assets/js/lib/api/alertingSettings.js
user: login: nelsonkopliku
body: **nit**
```suggestion
const alertingUri = '/settings/alerting';
```
created_at: 2025-05-21T07:53:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,19 @@
+import { faker } from '@faker-js/faker';
+import { Factory } from 'fishery';
+
+const baseAlertingSettingsFactory = Factory.define(() => ({
+  alertingEnabled: true,
+  smtpServer: faker.internet.domainName(),
+  smtpPort: faker.number.int({ min: 1, max: 65535 }),
+  smtpUsername: faker.internet.username(),
+  senderEmail: faker.internet.email(),
+  recipientEmail: faker.internet.email(),
+}));
+
+export const alertingSettingsFactory = baseAlertingSettingsFactory.params({
+  enforcedFromEnv: false,
+});
+
+export const alertingSettingsSendFactory = baseAlertingSettingsFactory.params({
path: assets/js/lib/test-utils/factories/alertingSettings.js
user: login: nelsonkopliku
body: I did not immediately grok what `alertingSettingsSendFactory` meant 😅 
What about something different like the following, if it fits your intent.
```suggestion
export const saveAlertingSettingsRequestFactory = baseAlertingSettingsFactory.params({
```
created_at: 2025-05-21T07:57:01Z
author_association: MEMBER
path: assets/js/pages/SettingsPage/SettingsPage.test.jsx
user: login: nelsonkopliku
body: I am afraid we're missing tests for the new Alerting Settings section.
created_at: 2025-05-21T07:58:45Z
author_association: MEMBER
path: assets/js/pages/SettingsPage/hooks.js
user: login: nelsonkopliku
body: Let's add tests for the new hook as well
created_at: 2025-05-21T07:59:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+    enforcedFromEnv = false,
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  const alertingFields = [
+    {
+      label: 'SMTP Server',
+      value: smtpServer,
+      ariaLabel: 'smtp-server',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'SMTP Port',
+      value: smtpPort,
+      ariaLabel: 'smtp-port',
+    },
+    {
+      label: 'SMTP User',
+      value: smtpUsername,
+      ariaLabel: 'smtp-username',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Password',
+      value: '•••••',
+      ariaLabel: 'smtp-password',
+    },
+    {
+      label: 'Alerting Sender',
+      value: senderEmail,
+      ariaLabel: 'alerting-sender',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Alerting Recipient',
+      value: recipientEmail,
+      ariaLabel: 'alerting-recipient',
+    },
+    {
+      label: 'Send Email Alerts',
+      value: alertingEnabled ? 'Enabled' : 'Disabled',
+      ariaLabel: 'alerting-enabled',
+    },
+  ];
+
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Tooltip
+              isEnabled={enforcedFromEnv}
+              content={ENFORCED_FROM_ENV_MESSAGE}
+              place="bottom"
+            >
+              <Button
+                type="primary-white-fit"
+                size="small"
+                aria-label="alerting-edit-button"
+                onClick={onEditClick}
+                disabled={enforcedFromEnv}
+              >
+                Edit Settings
+              </Button>
+            </Tooltip>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 gap-y-3 mt-5 items-center">
+        {alertingFields.map(({ label, value, ariaLabel, addClasses }) => (
+          <AlertingSettingsRow
+            key={ariaLabel}
+            label={label}
+            value={value}
+            ariaLabel={ariaLabel}
+            addClasses={addClasses}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+function AlertingSettingsRow({ label, value, ariaLabel, addClasses = '' }) {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: EMaksy
body: Why not to move to an extra component?

You could have a common story for it?
created_at: 2025-05-21T08:04:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  beforeAll(() => {
+    jest.restoreAllMocks();
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: skrech
body: Good catch, no it's not.
created_at: 2025-05-21T10:40:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { faker } from '@faker-js/faker';
+import { Factory } from 'fishery';
+
+const baseAlertingSettingsFactory = Factory.define(() => ({
+  alertingEnabled: true,
+  smtpServer: faker.internet.domainName(),
+  smtpPort: faker.number.int({ min: 1, max: 65535 }),
+  smtpUsername: faker.internet.username(),
+  senderEmail: faker.internet.email(),
+  recipientEmail: faker.internet.email(),
+}));
+
+export const alertingSettingsFactory = baseAlertingSettingsFactory.params({
+  enforcedFromEnv: false,
+});
+
+export const alertingSettingsSendFactory = baseAlertingSettingsFactory.params({
path: assets/js/lib/test-utils/factories/alertingSettings.js
user: login: skrech
body: Yes, I had hard time coming up with a name for that. I didn't want to have 'request' in the name but since there is only one usage for that factory now, I'll go with your suggestion.
created_at: 2025-05-21T11:54:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: EMaksy
body: I would consider to move destructuring inside the function 

```
export default function AlertingSettingsConfig({
  settings,
  userAbilities = [],
  onEditClick = noop,
}) 
{
    const {
    alertingEnabled = false,
    smtpServer = 'https://.....',
    smtpPort = 587,
    smtpUsername = '.....',
    senderEmail = '...@...',
    recipientEmail = '...@...',
    enforcedFromEnv = false,
  } = settings || {};
```

Also handling the null case would be great 
```
<AlertingSettingsConfig
  settings={null}
``` as it would crash in this case
created_at: 2025-05-22T09:15:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
+      smtpUsername,
+      senderEmail,
+      recipientEmail,
+      ...(editingPassword && { smtpPassword }),
+    };
+    onSave(settingsPayload);
+  }
+
+  return (
+    <Modal title="Enter Alerting Settings" open={open} onClose={onCancel}>
+      <form onSubmit={onSubmit}>
+        <div className="grid grid-cols-6 my-5 gap-6">
+          <Label className="col-span-2" htmlFor="alerting-enabled-input">
+            Send Email Alerts
+          </Label>
+          <div className="col-span-4">
+            <Switch
+              id="alerting-enabled-input"
+              name="alerting-enabled-input"
+              selected={alertingEnabled}
+              onChange={(value) => {
+                setAlertingEnabled(value);
+              }}
+            />
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-server-input" required>
+            SMTP Server
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-server-input"
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: EMaksy
body: Nitpick: 
"smtp-server-input" ,"alerting-enabled-input", "smtp-password-label", "sender-email-input" and "recipient-email-input" are used  multiple times, these could be variables in case you need to adjust it you do it once :) 
created_at: 2025-05-22T09:51:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
+      smtpUsername,
+      senderEmail,
+      recipientEmail,
+      ...(editingPassword && { smtpPassword }),
+    };
+    onSave(settingsPayload);
+  }
+
+  return (
+    <Modal title="Enter Alerting Settings" open={open} onClose={onCancel}>
+      <form onSubmit={onSubmit}>
+        <div className="grid grid-cols-6 my-5 gap-6">
+          <Label className="col-span-2" htmlFor="alerting-enabled-input">
+            Send Email Alerts
+          </Label>
+          <div className="col-span-4">
+            <Switch
+              id="alerting-enabled-input"
+              name="alerting-enabled-input"
+              selected={alertingEnabled}
+              onChange={(value) => {
+                setAlertingEnabled(value);
+              }}
+            />
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-server-input" required>
+            SMTP Server
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-server-input"
+              name="smtp-server-input"
+              value={smtpServer}
+              placeholder="Enter a URL"
+              error={hasError('smtp_server', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpServer(value);
+              }}
+            />
+            {hasError('smtp_server', errors) && (
+              <p
+                aria-label="smtp-server-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_server', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-port-input" required>
+            SMTP Port
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-port-input"
+              name="smtp-port-input"
+              value={smtpPort}
+              placeholder={587}
+              error={hasError('smtp_port', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpPort(value);
+              }}
+            />
+            {hasError('smtp_port', errors) && (
+              <p
+                aria-label="smtp-port-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_port', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-username-input" required>
+            SMTP Username
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-username-input"
+              name="smtp-username-input"
+              value={smtpUsername}
+              placeholder="Enter SMTP Username"
+              error={hasError('smtp_username', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpUsername(value);
+              }}
+            />
+            {hasError('smtp_username', errors) && (
+              <p
+                aria-label="smtp-username-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_username', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            id="smtp-password-label"
+            htmlFor="smtp-password-input"
+            required
+          >
+            SMTP Password
+          </Label>
+          {editingPassword ? (
+            <div className="col-span-4">
+              <Password
+                aria-labelledby="smtp-password-label"
+                name="smtp-password-input"
+                placeholder="Enter SMTP Password"
+                value={smtpPassword}
+                error={hasError('smtp_password', errors)}
+                onChange={({ target: { value } }) => {
+                  setSmtpPassword(value);
+                }}
+              />
+              {hasError('smtp_password', errors) && (
+                <p
+                  aria-label="smtp-password-input-error"
+                  role="alert"
+                  className="text-red-500 mt-1"
+                >
+                  {capitalize(getError('smtp_password', errors))}
+                </p>
+              )}
+            </div>
+          ) : (
+            <div className="col-span-4 border border-gray-200 p-5 rounded-md">
+              <p
+                aria-labelledby="smtp-password-label"
+                className="inline align-sub leading-10"
+              >
+                •••••
+              </p>
+              <Button
+                className="float-right"
+                type="danger"
+                onClick={() => setEditingPassword(true)}
+              >
+                Remove
+              </Button>
+            </div>
+          )}
+
+          <Label className="col-span-2" htmlFor="sender-email-input" required>
+            Alert Sender
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="sender-email-input"
+              name="sender-email-input"
+              value={senderEmail}
+              placeholder="Enter an email address"
+              error={hasError('sender_email', errors)}
+              onChange={({ target: { value } }) => {
+                setSenderEmail(value);
+              }}
+            />
+            {hasError('sender_email', errors) && (
+              <p
+                aria-label="sender-email-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('sender_email', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            htmlFor="recipient-email-input"
+            required
+          >
+            Alert Recipient
+          </Label>
+          <div className="col-span-4">
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: EMaksy
body: Nitpick: 
This strucutre repeats more or less 7 times

```
<Label className="col-span-2" htmlFor="field-id" required>
  Field Label
</Label>
<div className="col-span-4">
  <Input
    id="field-id"
    value={...}
    onChange={...}
    error={...}
    ...
  />
  {error && <p className="text-red-500">...</p>}
</div>
```

We rely quite a lot on this structure also in other Input Modals like in SuseManagerSettingsModal. 

It's out of scope for this PR, but we could consider extracting it into a reusable FormField component. Might be worth tracking as tech debt what do you think?
created_at: 2025-05-22T10:02:50Z
author_association: MEMBER
diff_hunk: @@ -299,36 +320,70 @@ function SettingsPage() {
       </section>
 
       <section>
-        <SettingsLoader
-          sectionName="Activity Logs"
-          status={calculateSettingsLoaderStatus(
-            activityLogsSettingsLoading,
-            activityLogsSettingsNetworkError
-          )}
-          onRetry={() => dispatch(fetchActivityLogsSettings())}
-        >
-          <ActivityLogsConfig
-            userAbilities={abilities}
-            retentionTime={activityLogsSettings.retention_time}
-            onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Activity Logs"
+            status={calculateSettingsLoaderStatus(
+              activityLogsSettingsLoading,
+              activityLogsSettingsNetworkError
+            )}
+            onRetry={() => dispatch(fetchActivityLogsSettings())}
+          >
+            <ActivityLogsConfig
+              userAbilities={abilities}
+              retentionTime={activityLogsSettings.retention_time}
+              onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+            />
+          </SettingsLoader>
+          <ActivityLogsSettingsModal
+            key={`${JSON.stringify(
+              activityLogsSettings
+            )}-${editingActivityLogsSettings}`}
+            open={editingActivityLogsSettings}
+            errors={activityLogsValidationErrors}
+            loading={activityLogsSettingsLoading}
+            initialRetentionTime={activityLogsSettings.retention_time}
+            onSave={(payload) => {
+              dispatch(updateActivityLogsSettings(payload));
+            }}
+            onCancel={() => {
+              dispatch(setActivityLogsSettingsErrors([]));
+              dispatch(setEditingActivityLogsSettings(false));
+            }}
           />
-        </SettingsLoader>
-        <ActivityLogsSettingsModal
-          key={`${JSON.stringify(
-            activityLogsSettings
-          )}-${editingActivityLogsSettings}`}
-          open={editingActivityLogsSettings}
-          errors={activityLogsValidationErrors}
-          loading={activityLogsSettingsLoading}
-          initialRetentionTime={activityLogsSettings.retention_time}
-          onSave={(payload) => {
-            dispatch(updateActivityLogsSettings(payload));
-          }}
-          onCancel={() => {
-            dispatch(setActivityLogsSettingsErrors([]));
-            dispatch(setEditingActivityLogsSettings(false));
-          }}
-        />
+        </div>
+      </section>
+
+      <section>
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Alerting Settings"
+            status={calculateSettingsLoaderStatus(
+              alertingFetchLoading,
+              alertingFetchError
+            )}
+            onRetry={() => fetchAlertingSettings()}
+          >
+            <AlertingSettingsConfig
+              settings={alertingSettings}
+              userAbilities={abilities}
+              onEditClick={() => {
+                clearAlertingSubmitErrors();
+                setAlertingSettingsModalOpen(true);
+              }}
+            />
+          </SettingsLoader>
+
+          <AlertingSettingsModal
+            key={`alertingmodal-${Object.values(alertingSettings).join('-')}-${alertingSettingsModalOpen}`}
path: assets/js/pages/SettingsPage/SettingsPage.jsx
user: login: EMaksy
body: After having a look, do we really need the key here ? 
I think the use effect mounts properly and we can remove the key. What do you think ? 
created_at: 2025-05-22T10:14:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,103 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Button
+              type="primary-white-fit"
+              size="small"
+              aria-label="alerting-edit-button"
+              onClick={onEditClick}
+            >
+              Edit Settings
+            </Button>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 mt-5 items-center">
+        <div className="font-bold mb-3">SMTP Server</div>
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: skrech
body: Done
created_at: 2025-05-23T12:39:52Z
author_association: CONTRIBUTOR
path: assets/js/pages/SettingsPage/SettingsPage.test.jsx
user: login: skrech
body: Added these tests.
created_at: 2025-05-23T12:41:43Z
author_association: CONTRIBUTOR
path: assets/js/pages/SettingsPage/hooks.js
user: login: skrech
body: Added these tests
created_at: 2025-05-23T12:41:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+    enforcedFromEnv = false,
+  } = {},
+  userAbilities = [],
+  onEditClick = noop,
+}) {
+  const alertingFields = [
+    {
+      label: 'SMTP Server',
+      value: smtpServer,
+      ariaLabel: 'smtp-server',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'SMTP Port',
+      value: smtpPort,
+      ariaLabel: 'smtp-port',
+    },
+    {
+      label: 'SMTP User',
+      value: smtpUsername,
+      ariaLabel: 'smtp-username',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Password',
+      value: '•••••',
+      ariaLabel: 'smtp-password',
+    },
+    {
+      label: 'Alerting Sender',
+      value: senderEmail,
+      ariaLabel: 'alerting-sender',
+      addClasses: 'truncate pr-12',
+    },
+    {
+      label: 'Alerting Recipient',
+      value: recipientEmail,
+      ariaLabel: 'alerting-recipient',
+    },
+    {
+      label: 'Send Email Alerts',
+      value: alertingEnabled ? 'Enabled' : 'Disabled',
+      ariaLabel: 'alerting-enabled',
+    },
+  ];
+
+  return (
+    <div className="container max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 bg-white dark:bg-gray-800 rounded-lg">
+      <div>
+        <h2 className="text-2xl font-bold inline-block">Email Alerts</h2>
+
+        <span className="float-right">
+          <DisabledGuard
+            userAbilities={userAbilities}
+            permitted={alertingSettingsPermittedFor}
+          >
+            <Tooltip
+              isEnabled={enforcedFromEnv}
+              content={ENFORCED_FROM_ENV_MESSAGE}
+              place="bottom"
+            >
+              <Button
+                type="primary-white-fit"
+                size="small"
+                aria-label="alerting-edit-button"
+                onClick={onEditClick}
+                disabled={enforcedFromEnv}
+              >
+                Edit Settings
+              </Button>
+            </Tooltip>
+          </DisabledGuard>
+        </span>
+      </div>
+
+      <p className="mt-3 mb-3 text-gray-500">
+        Settings used to send Trento system e-mails.
+      </p>
+
+      <div className="grid grid-cols-6 gap-y-3 mt-5 items-center">
+        {alertingFields.map(({ label, value, ariaLabel, addClasses }) => (
+          <AlertingSettingsRow
+            key={ariaLabel}
+            label={label}
+            value={value}
+            ariaLabel={ariaLabel}
+            addClasses={addClasses}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+function AlertingSettingsRow({ label, value, ariaLabel, addClasses = '' }) {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: skrech
body: To me this AlertingSettingsRow feels like supplementary component, just to lower the duplication. I don't think it needs to be extracted any futher. Additionally, it won't be used in any other component.  
created_at: 2025-05-23T12:44:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: skrech
body: For the destructuring part: I personally feel that leaving the destructuring in the function signature speaks more. In addition, I tried to make a quick search on the Internet on style guides and found this: https://github.com/airbnb/javascript?tab=readme-ov-file#destructuring which seems to classify destructuring in signatures as "best". Ultimately, this seems like a style/preferences type of question, maybe I could leave it like that?

For the second remark: It would also fail if we supply `1` or `"settings"`. For me, `undefined` (eg. not supplied at all) is the only case worth handling apart from providing the expected object. `undefined` is handled already in the function signature destructuring.
created_at: 2025-05-23T13:24:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
+      smtpUsername,
+      senderEmail,
+      recipientEmail,
+      ...(editingPassword && { smtpPassword }),
+    };
+    onSave(settingsPayload);
+  }
+
+  return (
+    <Modal title="Enter Alerting Settings" open={open} onClose={onCancel}>
+      <form onSubmit={onSubmit}>
+        <div className="grid grid-cols-6 my-5 gap-6">
+          <Label className="col-span-2" htmlFor="alerting-enabled-input">
+            Send Email Alerts
+          </Label>
+          <div className="col-span-4">
+            <Switch
+              id="alerting-enabled-input"
+              name="alerting-enabled-input"
+              selected={alertingEnabled}
+              onChange={(value) => {
+                setAlertingEnabled(value);
+              }}
+            />
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-server-input" required>
+            SMTP Server
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-server-input"
+              name="smtp-server-input"
+              value={smtpServer}
+              placeholder="Enter a URL"
+              error={hasError('smtp_server', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpServer(value);
+              }}
+            />
+            {hasError('smtp_server', errors) && (
+              <p
+                aria-label="smtp-server-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_server', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-port-input" required>
+            SMTP Port
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-port-input"
+              name="smtp-port-input"
+              value={smtpPort}
+              placeholder={587}
+              error={hasError('smtp_port', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpPort(value);
+              }}
+            />
+            {hasError('smtp_port', errors) && (
+              <p
+                aria-label="smtp-port-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_port', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-username-input" required>
+            SMTP Username
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-username-input"
+              name="smtp-username-input"
+              value={smtpUsername}
+              placeholder="Enter SMTP Username"
+              error={hasError('smtp_username', errors)}
+              onChange={({ target: { value } }) => {
+                setSmtpUsername(value);
+              }}
+            />
+            {hasError('smtp_username', errors) && (
+              <p
+                aria-label="smtp-username-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('smtp_username', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            id="smtp-password-label"
+            htmlFor="smtp-password-input"
+            required
+          >
+            SMTP Password
+          </Label>
+          {editingPassword ? (
+            <div className="col-span-4">
+              <Password
+                aria-labelledby="smtp-password-label"
+                name="smtp-password-input"
+                placeholder="Enter SMTP Password"
+                value={smtpPassword}
+                error={hasError('smtp_password', errors)}
+                onChange={({ target: { value } }) => {
+                  setSmtpPassword(value);
+                }}
+              />
+              {hasError('smtp_password', errors) && (
+                <p
+                  aria-label="smtp-password-input-error"
+                  role="alert"
+                  className="text-red-500 mt-1"
+                >
+                  {capitalize(getError('smtp_password', errors))}
+                </p>
+              )}
+            </div>
+          ) : (
+            <div className="col-span-4 border border-gray-200 p-5 rounded-md">
+              <p
+                aria-labelledby="smtp-password-label"
+                className="inline align-sub leading-10"
+              >
+                •••••
+              </p>
+              <Button
+                className="float-right"
+                type="danger"
+                onClick={() => setEditingPassword(true)}
+              >
+                Remove
+              </Button>
+            </div>
+          )}
+
+          <Label className="col-span-2" htmlFor="sender-email-input" required>
+            Alert Sender
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="sender-email-input"
+              name="sender-email-input"
+              value={senderEmail}
+              placeholder="Enter an email address"
+              error={hasError('sender_email', errors)}
+              onChange={({ target: { value } }) => {
+                setSenderEmail(value);
+              }}
+            />
+            {hasError('sender_email', errors) && (
+              <p
+                aria-label="sender-email-input-error"
+                role="alert"
+                className="text-red-500 mt-1"
+              >
+                {capitalize(getError('sender_email', errors))}
+              </p>
+            )}
+          </div>
+
+          <Label
+            className="col-span-2"
+            htmlFor="recipient-email-input"
+            required
+          >
+            Alert Recipient
+          </Label>
+          <div className="col-span-4">
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: skrech
body: I see your point. However, I lean more on the side of leaving this as it is now. Although repeated several times, these elements are fairly simple -- Label combined with an Input. Most of the code would have to be supplied as an argument, anyway. Almost half of the code is CSS fields that are concretely related to our current layout. The error part is a good candidate for "bundling" all these elements into their own component... but the current implementation depends on naming convention (for the error fields) that is different than the convention for JS variable names, meaning that we have to pass in these values separately.

All in all, we'll have fairly simple composite component that has a function signature accepting 10-20 arguments and at the same time be very rigid, working in only this specific case. I think, it's more beneficial to leave such elements as separate components which gives them more flexibility. 
created_at: 2025-05-23T13:48:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -299,36 +320,70 @@ function SettingsPage() {
       </section>
 
       <section>
-        <SettingsLoader
-          sectionName="Activity Logs"
-          status={calculateSettingsLoaderStatus(
-            activityLogsSettingsLoading,
-            activityLogsSettingsNetworkError
-          )}
-          onRetry={() => dispatch(fetchActivityLogsSettings())}
-        >
-          <ActivityLogsConfig
-            userAbilities={abilities}
-            retentionTime={activityLogsSettings.retention_time}
-            onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Activity Logs"
+            status={calculateSettingsLoaderStatus(
+              activityLogsSettingsLoading,
+              activityLogsSettingsNetworkError
+            )}
+            onRetry={() => dispatch(fetchActivityLogsSettings())}
+          >
+            <ActivityLogsConfig
+              userAbilities={abilities}
+              retentionTime={activityLogsSettings.retention_time}
+              onEditClick={() => dispatch(setEditingActivityLogsSettings(true))}
+            />
+          </SettingsLoader>
+          <ActivityLogsSettingsModal
+            key={`${JSON.stringify(
+              activityLogsSettings
+            )}-${editingActivityLogsSettings}`}
+            open={editingActivityLogsSettings}
+            errors={activityLogsValidationErrors}
+            loading={activityLogsSettingsLoading}
+            initialRetentionTime={activityLogsSettings.retention_time}
+            onSave={(payload) => {
+              dispatch(updateActivityLogsSettings(payload));
+            }}
+            onCancel={() => {
+              dispatch(setActivityLogsSettingsErrors([]));
+              dispatch(setEditingActivityLogsSettings(false));
+            }}
           />
-        </SettingsLoader>
-        <ActivityLogsSettingsModal
-          key={`${JSON.stringify(
-            activityLogsSettings
-          )}-${editingActivityLogsSettings}`}
-          open={editingActivityLogsSettings}
-          errors={activityLogsValidationErrors}
-          loading={activityLogsSettingsLoading}
-          initialRetentionTime={activityLogsSettings.retention_time}
-          onSave={(payload) => {
-            dispatch(updateActivityLogsSettings(payload));
-          }}
-          onCancel={() => {
-            dispatch(setActivityLogsSettingsErrors([]));
-            dispatch(setEditingActivityLogsSettings(false));
-          }}
-        />
+        </div>
+      </section>
+
+      <section>
+        <div className="pb-4">
+          <SettingsLoader
+            sectionName="Alerting Settings"
+            status={calculateSettingsLoaderStatus(
+              alertingFetchLoading,
+              alertingFetchError
+            )}
+            onRetry={() => fetchAlertingSettings()}
+          >
+            <AlertingSettingsConfig
+              settings={alertingSettings}
+              userAbilities={abilities}
+              onEditClick={() => {
+                clearAlertingSubmitErrors();
+                setAlertingSettingsModalOpen(true);
+              }}
+            />
+          </SettingsLoader>
+
+          <AlertingSettingsModal
+            key={`alertingmodal-${Object.values(alertingSettings).join('-')}-${alertingSettingsModalOpen}`}
path: assets/js/pages/SettingsPage/SettingsPage.jsx
user: login: skrech
body: To be honest, I'm not entirely sure.
I think we need this, since  we are using the values in `alertingSettings` as default values in `useState` expressions in `AlertingSettingsModal`. However, changing these values is only possible if we modify settings via the modal, in which case it would close itself after successful change.
Maybe the only thing needed is the `alertingSettingsModalOpen`. I'll try it now.
created_at: 2025-05-23T13:55:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
+      smtpUsername,
+      senderEmail,
+      recipientEmail,
+      ...(editingPassword && { smtpPassword }),
+    };
+    onSave(settingsPayload);
+  }
+
+  return (
+    <Modal title="Enter Alerting Settings" open={open} onClose={onCancel}>
+      <form onSubmit={onSubmit}>
+        <div className="grid grid-cols-6 my-5 gap-6">
+          <Label className="col-span-2" htmlFor="alerting-enabled-input">
+            Send Email Alerts
+          </Label>
+          <div className="col-span-4">
+            <Switch
+              id="alerting-enabled-input"
+              name="alerting-enabled-input"
+              selected={alertingEnabled}
+              onChange={(value) => {
+                setAlertingEnabled(value);
+              }}
+            />
+          </div>
+
+          <Label className="col-span-2" htmlFor="smtp-server-input" required>
+            SMTP Server
+          </Label>
+          <div className="col-span-4">
+            <Input
+              id="smtp-server-input"
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: skrech
body: To me, it seems too protective. For example, `smtp-sever-input` is used only 3 time. Two times for the bidirectinal HTML label attachment and as NAME for the input element. The NAME is the same only because I don't have a better name to put. :D It'd be fine for ID and NAME to be different.
created_at: 2025-05-23T14:03:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: nelsonkopliku
body: nothing against destructuring in function signature
created_at: 2025-05-23T14:27:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,127 @@
+import React from 'react';
+import { noop } from 'lodash';
+
+import Button from '@common/Button';
+import DisabledGuard from '@common/DisabledGuard';
+import Tooltip from '@common/Tooltip';
+
+const alertingSettingsPermittedFor = ['all:alerting_settings'];
+
+export const ENFORCED_FROM_ENV_MESSAGE =
+  'Alerting settings are enforced by environment variables';
+
+export default function AlertingSettingsConfig({
+  settings: {
+    alertingEnabled = false,
+    smtpServer = 'https://.....',
+    smtpPort = 587,
+    smtpUsername = '.....',
+    senderEmail = '...@...',
+    recipientEmail = '...@...',
+    enforcedFromEnv = false,
+  } = {},
+  userAbilities = [],
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.jsx
user: login: nelsonkopliku
body: Not for this particular case but keep in mind this https://github.com/facebook/react/issues/18123#issuecomment-591978544

TL;DR: if an array prop is used inside a `useEffect` we'll have to define the empty array as a const
created_at: 2025-05-23T14:29:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+import AlertingSettingsConfig from './AlertingSettingsConfig';
+
+export default {
+  title: 'Components/AlertingSettingsConfig',
+  component: AlertingSettingsConfig,
+  argTypes: {
+    settings: {
+      description: 'Current alerting settings values',
+      control: {
+        type: 'object',
+      },
+    },
+
+    userAbilities: {
+      description: 'Abilities of the current user',
+      control: {
+        type: 'array',
+      },
+    },
+
+    onEditClick: {
+      description: 'Callback that would run on edit button being clicked',
+      control: { type: 'function' },
+    },
+  },
+};
+
+export const Default = {};
+
+export const WithFilledInValues = {
+  args: {
+    settings: {
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.stories.jsx
user: login: nelsonkopliku
body: **nit:** we could use the factory here.
created_at: 2025-05-23T14:32:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+
+import { alertingSettingsFactory } from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsConfig, {
+  ENFORCED_FROM_ENV_MESSAGE,
+} from './AlertingSettingsConfig';
+
+const adminUser = [{ name: 'all', resource: 'all' }];
+
+describe('AlertingSettingsConfig', () => {
+  it('renders all visual props when called with default arguments', () => {
+    render(<AlertingSettingsConfig />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(
+      'https://...'
+    );
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent('587');
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent('...');
+    expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      '...@...'
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Disabled'
+    );
+  });
+
+  it('renders all visual props supplied as arguments', () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    render(<AlertingSettingsConfig settings={alertingSettings} />);
+
+    expect(screen.getByLabelText('smtp-server')).toHaveTextContent(smtpServer);
+    expect(screen.getByLabelText('smtp-port')).toHaveTextContent(
+      smtpPort.toString()
+    );
+    expect(screen.getByLabelText('smtp-username')).toHaveTextContent(
+      smtpUsername
+    );
+    expect(screen.getByLabelText('smtp-password')).toHaveTextContent('•••••');
+    expect(screen.getByLabelText('alerting-sender')).toHaveTextContent(
+      senderEmail
+    );
+    expect(screen.getByLabelText('alerting-recipient')).toHaveTextContent(
+      recipientEmail
+    );
+    expect(screen.getByLabelText('alerting-enabled')).toHaveTextContent(
+      'Enabled'
+    );
+  });
+
+  it.each`
+    case                        | abilities
+    ${'not set'}                | ${undefined}
+    ${'empty list'}             | ${[]}
+    ${'non-relevant abilities'} | ${[{ name: 'all', resource: 'some_resource' }]}
+  `(
+    'forbids editing when user has not sufficient abilities ($case)',
+    async ({ abilities }) => {
+      const onEditClick = jest.fn();
+      const user = userEvent.setup();
+
+      render(
+        <AlertingSettingsConfig
+          onEditClick={onEditClick}
+          userAbilities={abilities}
+        />
+      );
+
+      const editButton = screen.getByLabelText('alerting-edit-button');
+      expect(editButton).toBeDisabled();
+
+      await user.click(editButton);
+      expect(onEditClick).not.toHaveBeenCalled();
+
+      await user.hover(editButton);
+      expect(
+        screen.queryByText('You are not authorized for this action')
+      ).toBeVisible();
+    }
+  );
+
+  it.each`
+    case                       | abilities
+    ${'admin '}                | ${adminUser}
path: assets/js/common/AlertingSettingsConfig/AlertingSettingsConfig.test.jsx
user: login: nelsonkopliku
body: **nit:** is the extra space intentional?
```suggestion
    ${'admin'}                | ${adminUser}
```
created_at: 2025-05-23T14:33:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,225 @@
+import React from 'react';
+import { faker } from '@faker-js/faker';
+import { act, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { capitalize } from 'lodash';
+
+import {
+  alertingSettingsFactory,
+  alertingSettingsSaveRequestFactory,
+} from '@lib//test-utils/factories/alertingSettings';
+
+import AlertingSettingsModal from './AlertingSettingsModal';
+
+describe('AlertingSettingsModal', () => {
+  it('renders correctly when opened with no arguments passed', async () => {
+    await act(() => {
+      render(<AlertingSettingsModal open />);
+    });
+
+    const alertingEnabled = screen.getByRole('switch', {
+      name: 'Send Email Alerts',
+    });
+    expect(alertingEnabled).not.toBeChecked();
+
+    const smtpServer = screen.getByRole('textbox', { name: 'SMTP Server *' });
+    expect(smtpServer).toHaveAttribute('placeholder', 'Enter a URL');
+    expect(smtpServer).toHaveTextContent('');
+
+    const smtpPort = screen.getByRole('textbox', { name: 'SMTP Port *' });
+    expect(smtpPort).toHaveAttribute('placeholder', '587');
+    expect(smtpPort).toHaveTextContent('');
+
+    const smtpUsername = screen.getByRole('textbox', {
+      name: 'SMTP Username *',
+    });
+    expect(smtpUsername).toHaveAttribute('placeholder', 'Enter SMTP Username');
+    expect(smtpUsername).toHaveTextContent('');
+
+    const smtpPassword = screen.getByLabelText(/^SMTP Password/, {
+      selector: ':placeholder-shown',
+    });
+    expect(smtpPassword).toHaveAttribute('placeholder', 'Enter SMTP Password');
+
+    const emailSender = screen.getByRole('textbox', { name: 'Alert Sender *' });
+    expect(emailSender).toHaveAttribute(
+      'placeholder',
+      'Enter an email address'
+    );
+    expect(emailSender).toHaveTextContent('');
+
+    const emailRecipient = screen.getByRole('textbox', {
+      name: 'Alert Recipient *',
+    });
+    expect(emailRecipient).toHaveAttribute(
+      'placeholder',
+      'Enter an email address'
+    );
+    expect(emailRecipient).toHaveTextContent('');
+  });
+
+  it('renders correctly when opened with supplied settings arg', async () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    await act(() => {
+      render(
+        <AlertingSettingsModal previousSettings={alertingSettings} open />
+      );
+    });
+
+    expect(
+      screen.getByRole('switch', { name: 'Send Email Alerts' })
+    ).toBeChecked();
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Server *' })
+    ).toHaveDisplayValue(smtpServer);
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Port *' })
+    ).toHaveDisplayValue(smtpPort.toString());
+    expect(
+      screen.getByRole('textbox', { name: 'SMTP Username *' })
+    ).toHaveDisplayValue(smtpUsername);
+    expect(screen.getByLabelText(/^SMTP Password/)).toHaveTextContent('•••••');
+    expect(
+      screen.getByRole('textbox', { name: 'Alert Sender *' })
+    ).toHaveDisplayValue(senderEmail);
+    expect(
+      screen.getByRole('textbox', { name: 'Alert Recipient *' })
+    ).toHaveDisplayValue(recipientEmail);
+  });
+
+  it('calls onSave handler correctly when supplying fresh settings', async () => {
+    const {
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      smtpPassword,
+      senderEmail,
+      recipientEmail,
+    } = alertingSettingsSaveRequestFactory.build();
+
+    const user = userEvent.setup();
+    const onSave = jest.fn();
+
+    render(<AlertingSettingsModal open onSave={onSave} />);
+
+    await user.click(screen.getByRole('switch', 'Email Alerts'));
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Server *' }),
+      smtpServer
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Port *' }),
+      smtpPort.toString()
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'SMTP Username *' }),
+      smtpUsername
+    );
+    await user.type(screen.getByLabelText(/^SMTP Password/), smtpPassword);
+    await user.type(
+      screen.getByRole('textbox', { name: 'Alert Sender *' }),
+      senderEmail
+    );
+    await user.type(
+      screen.getByRole('textbox', { name: 'Alert Recipient *' }),
+      recipientEmail
+    );
+    await user.click(screen.getByRole('button', { name: 'Save Settings' }));
+
+    expect(onSave).toBeCalledWith({
+      alertingEnabled: true,
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      smtpPassword,
+      senderEmail,
+      recipientEmail,
+    });
+  });
+
+  it('does not supply password to onSave handler by default when previous settings', async () => {
+    const alertingSettings = alertingSettingsFactory.build();
+    const { smtpServer, smtpPort, smtpUsername, senderEmail, recipientEmail } =
+      alertingSettings;
+
+    const user = userEvent.setup();
+    const onSave = jest.fn();
+
+    render(
+      <AlertingSettingsModal
+        previousSettings={alertingSettings}
+        open
+        onSave={onSave}
+      />
+    );
+    await user.click(screen.getByRole('button', { name: 'Save Settings' }));
+
+    expect(onSave).toBeCalledWith({
+      alertingEnabled: true,
+      smtpServer,
+      smtpPort,
+      smtpUsername,
+      // No `smtpPassword'
+      senderEmail,
+      recipientEmail,
+    });
+  });
+
+  it('renders errors correctly', async () => {
+    function construct_error_message(pointer, errorDetails) {
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.test.jsx
user: login: nelsonkopliku
body: ```suggestion
    const constructErrorMessage = (pointer, errorDetails) {
```
created_at: 2025-05-23T14:40:52Z
author_association: MEMBER
diff_hunk: @@ -35,7 +35,15 @@ const getButtonClasses = (type) => {
   }
 };
 
-function Button({ children, className, type, size, disabled, ...props }) {
+function Button({
+  children,
+  className,
+  type,
+  size,
+  disabled,
+  isSubmitButton,
path: assets/js/common/Button/Button.jsx
user: login: nelsonkopliku
body: nit: we're already in a Button context, so the Button suffix might be redundant
```suggestion
  asSubmit = false,
```
Additionally it might deserve at least one extra test, but no strong opinion.
created_at: 2025-05-23T14:42:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: nelsonkopliku
body: ```suggestion
  const onSubmit = (e) {
```
created_at: 2025-05-23T14:55:05Z
author_association: MEMBER
diff_hunk: @@ -316,14 +316,10 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Platform do
         type: :object,
         properties: %{
           enabled: %Schema{type: :boolean},
-          sender_email: %Schema{type: :string},
-          recipient_email: %Schema{type: :string},
+          sender_email: %Schema{type: :string, format: :email},
+          recipient_email: %Schema{type: :string, format: :email},
path: lib/trento_web/openapi/v1/schema/platform.ex
user: login: nelsonkopliku
body: You know what... It seems that this is not having any effect 😅 

In fact if we provide a non email, the open api spex cast and validate does not catch it, but it gets caught by the underlying ecto model 🤷 

Let's keep it nontheless, not sure whether a library bug or a library misuse from our side or what
created_at: 2025-05-23T15:35:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: nelsonkopliku
body: On initial transition smtpPort is `undefined`, hence `Number(smtpPort)` becomes `NaN` skipping the `omitBy(data, isNil)` transformation. 
This results in `smtp_port: null` being sent to the API and some weird validation behavior.

We might consider something like the following
```js
const port = Number(smtpPort);
const settingsPayload = {
  //...
  smtpPort: isNaN(port) ? null : port,
}
```
Or anyway something that consistently sends or does not send the value.

However, I realize that this change would need other changes and maybe we could address further related improvements in a followup PR.
created_at: 2025-05-23T15:42:42Z
author_association: MEMBER
diff_hunk: @@ -319,3 +327,144 @@ describe('useApiKeySettings', () => {
     expect(hookResult.current.apiKey).toBe('new_api_key');
   });
 });
+
+describe('useAlertingSettings', () => {
+  const saveAlertingSettings = alertingSettingsSaveRequestFactory.build();
+  const saveAlertingData =
+    alertingSettingsSaveRequestToApiData(saveAlertingSettings);
+
+  const settingsFromSaveSettings = (alertingSettings) =>
+    flow(
+      pick([
+        'alertingEnabled',
+        'smtpServer',
+        'smtpPort',
+        'smtpUsername',
+        'senderEmail',
+        'recipientEmail',
+      ]),
+      set('enforcedFromEnv', false)
+    )(alertingSettings);
+
+  const dataFromSaveData = (alertingData) =>
+    flow(
+      pick([
+        'enabled',
+        'smtp_server',
+        'smtp_port',
+        'smtp_username',
+        'sender_email',
+        'recipient_email',
+      ]),
+      set('enforced_from_env', false)
+    )(alertingData);
path: assets/js/pages/SettingsPage/hooks.test.jsx
user: login: nelsonkopliku
body: Besides naming not being immediately clear, to me at least, I am wondering: since these two functions are always called with the same constant arguments, couldn't they be just two more constants? 👀 

Or am I missing something?

```suggestion
  const fooNeedsBetterName = flow(
      pick([
        'alertingEnabled',
        'smtpServer',
        'smtpPort',
        'smtpUsername',
        'senderEmail',
        'recipientEmail',
      ]),
      set('enforcedFromEnv', false)
    )(saveAlertingSettings);

  const barNeedsBetterName = flow(
      pick([
        'enabled',
        'smtp_server',
        'smtp_port',
        'smtp_username',
        'sender_email',
        'recipient_email',
      ]),
      set('enforced_from_env', false)
    )(saveAlertingData);
```
created_at: 2025-05-23T16:06:06Z
author_association: MEMBER
diff_hunk: @@ -35,7 +35,15 @@ const getButtonClasses = (type) => {
   }
 };
 
-function Button({ children, className, type, size, disabled, ...props }) {
+function Button({
+  children,
+  className,
+  type,
+  size,
+  disabled,
+  isSubmitButton,
path: assets/js/common/Button/Button.jsx
user: login: skrech
body: Changed the name, we'll leave the test for later :(
created_at: 2025-05-29T09:07:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: skrech
body: I made changes that should cover this case. Added check for every string fields as well because when user inputs something and then deletes it, the empty fields is sent as `""` and this is not correct. This seems good enough protection for now.  
created_at: 2025-05-29T12:57:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,3 +327,144 @@ describe('useApiKeySettings', () => {
     expect(hookResult.current.apiKey).toBe('new_api_key');
   });
 });
+
+describe('useAlertingSettings', () => {
+  const saveAlertingSettings = alertingSettingsSaveRequestFactory.build();
+  const saveAlertingData =
+    alertingSettingsSaveRequestToApiData(saveAlertingSettings);
+
+  const settingsFromSaveSettings = (alertingSettings) =>
+    flow(
+      pick([
+        'alertingEnabled',
+        'smtpServer',
+        'smtpPort',
+        'smtpUsername',
+        'senderEmail',
+        'recipientEmail',
+      ]),
+      set('enforcedFromEnv', false)
+    )(alertingSettings);
+
+  const dataFromSaveData = (alertingData) =>
+    flow(
+      pick([
+        'enabled',
+        'smtp_server',
+        'smtp_port',
+        'smtp_username',
+        'sender_email',
+        'recipient_email',
+      ]),
+      set('enforced_from_env', false)
+    )(alertingData);
path: assets/js/pages/SettingsPage/hooks.test.jsx
user: login: skrech
body: Renamed the variables and made them constants. I think it's better now.
created_at: 2025-05-29T12:59:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,259 @@
+import React, { useState } from 'react';
+import { capitalize, noop, isEmpty } from 'lodash';
+
+import Button from '@common/Button';
+import Label from '@common/Label';
+import Modal from '@common/Modal';
+import Input, { Password } from '@common/Input';
+import Switch from '@common/Switch';
+
+import { hasError, getError } from '@lib/api/validationErrors';
+
+export default function AlertingSettingsModal({
+  previousSettings = {},
+  errors = [],
+  open = false,
+  loading = false,
+  onSave = noop,
+  onCancel = noop,
+}) {
+  const [alertingEnabled, setAlertingEnabled] = useState(
+    Boolean(previousSettings.alertingEnabled)
+  );
+  const [smtpServer, setSmtpServer] = useState(previousSettings.smtpServer);
+  const [smtpPort, setSmtpPort] = useState(previousSettings.smtpPort);
+  const [smtpUsername, setSmtpUsername] = useState(
+    previousSettings.smtpUsername
+  );
+  const [senderEmail, setSenderEmail] = useState(previousSettings.senderEmail);
+  const [recipientEmail, setRecipientEmail] = useState(
+    previousSettings.recipientEmail
+  );
+
+  const [editingPassword, setEditingPassword] = useState(
+    isEmpty(previousSettings)
+  );
+  const [smtpPassword, setSmtpPassword] = useState('');
+
+  function onSubmit(e) {
+    e.preventDefault();
+    const settingsPayload = {
+      alertingEnabled,
+      smtpServer,
+      smtpPort: Number(smtpPort),
path: assets/js/common/AlertingSettingsModal/AlertingSettingsModal.jsx
user: login: nelsonkopliku
body: thanks, that's good enough indeed. We might polish further when we get feedback
created_at: 2025-05-30T08:40:21Z
author_association: MEMBER

PR comments:
user: login: skrech
created_at: 2025-05-20T07:15:55Z
body: > Hey @skrech, good job! Before going deeper into the code I noticed a few visual/behavioral improvement opportunities.
> Edit Settings button misalignment
> Missing feedback when saving/updating settings fails
> Stale modal state
> 
> Happy to answer any question.

Thank you, Nelson.
All of these UX issues are addressed.
author_association: CONTRIBUTOR
user: login: skrech
created_at: 2025-05-23T14:10:07Z
body: I think I've addressed every code-related issue and responded to every remark. So, requesting re-review. :)

All the changes are into their own commit for easier reviewing.
author_association: CONTRIBUTOR