mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 16:42:15 +00:00
Label notification and settings switches
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, Show, For, Accessor, Setter } from 'solid-js';
|
||||
import { Component, Show, For, Accessor, Setter, createUniqueId } from 'solid-js';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { SectionHeader } from '@/components/shared/SectionHeader';
|
||||
@@ -31,6 +31,8 @@ export interface RecoverySettingsPanelProps {
|
||||
}
|
||||
|
||||
export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (props) => {
|
||||
const backupPollingTitleId = `settings-recovery-${createUniqueId()}-backup-polling-title`;
|
||||
|
||||
return (
|
||||
<SettingsPanel title="Recovery" noPadding bodyClass="divide-y divide-border">
|
||||
{/* Backup Polling Section */}
|
||||
@@ -57,7 +59,9 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
|
||||
{/* Enable toggle */}
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-base-content">Enable backup polling</p>
|
||||
<p id={backupPollingTitleId} class="text-sm font-medium text-base-content">
|
||||
Enable backup polling
|
||||
</p>
|
||||
<p class="text-xs text-muted">
|
||||
Required for workload backup status, storage snapshots, and alerting.
|
||||
</p>
|
||||
@@ -68,6 +72,7 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
|
||||
class="sr-only peer"
|
||||
checked={props.backupPollingEnabled()}
|
||||
disabled={props.backupPollingEnvLocked()}
|
||||
aria-labelledby={backupPollingTitleId}
|
||||
onChange={(e) => {
|
||||
props.setBackupPollingEnabled(e.currentTarget.checked);
|
||||
if (!props.backupPollingEnvLocked()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Show, For, Accessor, Setter } from 'solid-js';
|
||||
import { Component, Show, For, Accessor, Setter, createUniqueId } from 'solid-js';
|
||||
import SettingsPanel from '@/components/shared/SettingsPanel';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { HelpIcon } from '@/components/shared/HelpIcon';
|
||||
@@ -47,6 +47,7 @@ export interface UpdatesSettingsPanelProps {
|
||||
}
|
||||
|
||||
export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props) => {
|
||||
const autoUpdateTitleId = `settings-updates-${createUniqueId()}-auto-update-title`;
|
||||
const latestVersion = () => props.updateInfo()?.latestVersion;
|
||||
const dockerImageTag = () => buildDockerImageTag(latestVersion());
|
||||
const systemdDownloadCommand = () => buildLinuxAmd64DownloadCommand(latestVersion());
|
||||
@@ -88,11 +89,7 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
|
||||
}));
|
||||
|
||||
return (
|
||||
<SettingsPanel
|
||||
title={UPDATES_PANEL_COPY.title}
|
||||
noPadding
|
||||
bodyClass="divide-y divide-border"
|
||||
>
|
||||
<SettingsPanel title={UPDATES_PANEL_COPY.title} noPadding bodyClass="divide-y divide-border">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="space-y-4">
|
||||
{/* Version Status Section */}
|
||||
@@ -296,9 +293,9 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-base-content">
|
||||
<span id={autoUpdateTitleId} class="text-sm font-medium text-base-content">
|
||||
{UPDATES_PANEL_COPY.autoUpdateTitle}
|
||||
</label>
|
||||
</span>
|
||||
<p class="text-xs text-muted">{UPDATES_PANEL_COPY.autoUpdateDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,6 +309,7 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
disabled={autoUpdateLocked()}
|
||||
aria-labelledby={autoUpdateTitleId}
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 bg-surface-alt peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600 peer-disabled:opacity-50"></div>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { RecoverySettingsPanel } from '../RecoverySettingsPanel';
|
||||
import { UpdatesSettingsPanel } from '../UpdatesSettingsPanel';
|
||||
|
||||
describe('settings switch labels', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('associates the auto-update switch with its visible label', () => {
|
||||
const [updateChannel, setUpdateChannel] = createSignal<'stable' | 'rc'>('stable');
|
||||
const [autoUpdateEnabled, setAutoUpdateEnabled] = createSignal(false);
|
||||
const [autoUpdateCheckInterval, setAutoUpdateCheckInterval] = createSignal(24);
|
||||
const [autoUpdateTime, setAutoUpdateTime] = createSignal('03:00');
|
||||
|
||||
render(() => (
|
||||
<UpdatesSettingsPanel
|
||||
versionInfo={() => null}
|
||||
updateInfo={() => null}
|
||||
checkingForUpdates={() => false}
|
||||
updateChannel={updateChannel}
|
||||
setUpdateChannel={setUpdateChannel}
|
||||
autoUpdateEnabled={autoUpdateEnabled}
|
||||
setAutoUpdateEnabled={setAutoUpdateEnabled}
|
||||
autoUpdateCheckInterval={autoUpdateCheckInterval}
|
||||
setAutoUpdateCheckInterval={setAutoUpdateCheckInterval}
|
||||
autoUpdateTime={autoUpdateTime}
|
||||
setAutoUpdateTime={setAutoUpdateTime}
|
||||
checkForUpdates={vi.fn().mockResolvedValue(undefined)}
|
||||
setHasUnsavedChanges={vi.fn()}
|
||||
updatePlan={() => null}
|
||||
onInstallUpdate={vi.fn()}
|
||||
isInstalling={() => false}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByRole('checkbox', { name: 'Automatic Stable Updates' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('associates the backup polling switch with its visible label', () => {
|
||||
const [backupPollingEnabled, setBackupPollingEnabled] = createSignal(true);
|
||||
const [backupPollingInterval, setBackupPollingInterval] = createSignal(3600);
|
||||
const [backupPollingCustomMinutes, setBackupPollingCustomMinutes] = createSignal(60);
|
||||
const [backupPollingUseCustom, setBackupPollingUseCustom] = createSignal(false);
|
||||
const [showExportDialog, setShowExportDialog] = createSignal(false);
|
||||
const [showImportDialog, setShowImportDialog] = createSignal(false);
|
||||
const [useCustomPassphrase, setUseCustomPassphrase] = createSignal(false);
|
||||
|
||||
render(() => (
|
||||
<RecoverySettingsPanel
|
||||
backupPollingEnabled={backupPollingEnabled}
|
||||
setBackupPollingEnabled={setBackupPollingEnabled}
|
||||
backupPollingInterval={backupPollingInterval}
|
||||
setBackupPollingInterval={setBackupPollingInterval}
|
||||
backupPollingCustomMinutes={backupPollingCustomMinutes}
|
||||
setBackupPollingCustomMinutes={setBackupPollingCustomMinutes}
|
||||
backupPollingUseCustom={backupPollingUseCustom}
|
||||
setBackupPollingUseCustom={setBackupPollingUseCustom}
|
||||
backupPollingEnvLocked={() => false}
|
||||
backupIntervalSelectValue={() => '3600'}
|
||||
backupIntervalSummary={() => 'Every hour'}
|
||||
setHasUnsavedChanges={vi.fn()}
|
||||
showExportDialog={showExportDialog}
|
||||
setShowExportDialog={setShowExportDialog}
|
||||
showImportDialog={showImportDialog}
|
||||
setShowImportDialog={setShowImportDialog}
|
||||
setUseCustomPassphrase={setUseCustomPassphrase}
|
||||
securityStatus={() => ({ hasAuthentication: true })}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByRole('checkbox', { name: 'Enable backup polling' })).toBeChecked();
|
||||
expect(useCustomPassphrase()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,10 @@ describe('AlertAppriseDestinationsSection', () => {
|
||||
it('associates visible labels with CLI mode fields', () => {
|
||||
renderSection(makeConfig({ mode: 'cli' }));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Apprise notifications Enabled' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
expect(screen.getByRole('combobox', { name: 'Delivery mode' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'Delivery targets' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: 'CLI path' })).toBeInTheDocument();
|
||||
|
||||
@@ -51,6 +51,7 @@ interface AlertAppriseDestinationsSectionProps {
|
||||
export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsSectionProps) {
|
||||
const fieldIdPrefix = `alert-apprise-${createUniqueId()}`;
|
||||
const fieldIds = {
|
||||
title: `${fieldIdPrefix}-title`,
|
||||
targets: `${fieldIdPrefix}-targets`,
|
||||
cliPath: `${fieldIdPrefix}-cli-path`,
|
||||
serverUrl: `${fieldIdPrefix}-server-url`,
|
||||
@@ -63,6 +64,7 @@ export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsS
|
||||
|
||||
return (
|
||||
<SettingsPanel
|
||||
titleId={fieldIds.title}
|
||||
title={ALERT_DESTINATIONS_APPRISE_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_APPRISE_PANEL_DESCRIPTION}
|
||||
action={
|
||||
@@ -73,6 +75,7 @@ export function AlertAppriseDestinationsSection(props: AlertAppriseDestinationsS
|
||||
props.updateApprise({ enabled: event.currentTarget.checked });
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
ariaLabelledBy={fieldIds.title}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(props.config.enabled)}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AlertEmailDestinationsSection } from './AlertEmailDestinationsSection';
|
||||
import type { UIEmailConfig } from './types';
|
||||
|
||||
vi.mock('@/api/notifications', () => ({
|
||||
NotificationsAPI: {
|
||||
getEmailProviders: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
|
||||
function makeConfig(overrides: Partial<UIEmailConfig> = {}): UIEmailConfig {
|
||||
return {
|
||||
enabled: false,
|
||||
provider: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
to: [],
|
||||
tls: false,
|
||||
startTLS: true,
|
||||
replyTo: '',
|
||||
maxRetries: 3,
|
||||
retryDelay: 60,
|
||||
rateLimit: 10,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AlertEmailDestinationsSection', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('associates the visible panel heading with its status toggle', () => {
|
||||
render(() => (
|
||||
<AlertEmailDestinationsSection
|
||||
config={makeConfig()}
|
||||
setConfig={vi.fn()}
|
||||
setHasUnsavedChanges={vi.fn()}
|
||||
onTest={vi.fn()}
|
||||
testing={false}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Email notifications Disabled' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createUniqueId } from 'solid-js';
|
||||
|
||||
import { EmailProviderSelect } from '@/components/Alerts/EmailProviderSelect';
|
||||
import { SettingsPanel } from '@/components/shared/SettingsPanel';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
@@ -17,8 +19,11 @@ interface AlertEmailDestinationsSectionProps {
|
||||
}
|
||||
|
||||
export function AlertEmailDestinationsSection(props: AlertEmailDestinationsSectionProps) {
|
||||
const titleId = `alert-email-destinations-${createUniqueId()}-title`;
|
||||
|
||||
return (
|
||||
<SettingsPanel
|
||||
titleId={titleId}
|
||||
title={ALERT_DESTINATIONS_EMAIL_PANEL_TITLE}
|
||||
description={ALERT_DESTINATIONS_EMAIL_PANEL_DESCRIPTION}
|
||||
action={
|
||||
@@ -32,6 +37,7 @@ export function AlertEmailDestinationsSection(props: AlertEmailDestinationsSecti
|
||||
props.setHasUnsavedChanges(true);
|
||||
}}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={titleId}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertDestinationsStatusLabel(props.config.enabled)}
|
||||
|
||||
Reference in New Issue
Block a user