Settings: Save only; the Reset button is gone from every card

Reset sat next to Save and discarded a card's edits with one click and no
confirmation. The bar is now SaveBar; the Reset handlers go with it, as does the
Reset button on the Global Admin settings card. A failed save shows its reason
until the next Save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-10-30 12:27:31 +01:00
parent b631c63c53
commit 4fa439cc3c
22 changed files with 69 additions and 276 deletions
@@ -143,7 +143,6 @@ interface AdminConfigContextValue {
// Actions
handleSaveAdminConfig: () => Promise<void>;
handleResetAdminConfig: () => void;
handleSaveDiagPaths: (paths: DiagnosticsLogPath[]) => Promise<void>;
}
@@ -393,58 +392,6 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode })
}
}, [isGlobalAdmin, adminConfig, globalRateLimit, userRateLimit, portalUserRateLimit, globalAdminRateLimit, platformStatsBlobSasUrl, agentMigrateApiBaseUrl, agentMigrateTenantOverridesJson, collectorIdleTimeoutMinutes, desktopDetectorNoCandidateTimeoutMinutes, opsEventRetentionDays, slaNotificationCooldownHours, allowAgentDowngrade, enforceClientAppBinding, mcpClientRegistrationEnabled, modernDeploymentHarmlessEventIds, sessionDeletionKillSwitch, autoApproveNewTenants, selfServiceAppHomingEnabled, imeMsiArchivingEnabled, maxImeMsiDownloadSizeMB, getAccessToken]);
// Reset admin config
const handleResetAdminConfig = useCallback(() => {
if (!adminConfig) return;
setGlobalRateLimit(adminConfig.globalRateLimitRequestsPerMinute);
setUserRateLimit(adminConfig.userRateLimitRequestsPerMinute ?? 120);
setPortalUserRateLimit(adminConfig.portalUserRateLimitRequestsPerMinute ?? 600);
setGlobalAdminRateLimit(adminConfig.globalAdminRateLimitRequestsPerMinute ?? 600);
setPlatformStatsBlobSasUrl(adminConfig.platformStatsBlobSasUrl ?? "");
setAgentMigrateApiBaseUrl(adminConfig.agentMigrateApiBaseUrl ?? "");
setAgentMigrateTenantOverridesJson(adminConfig.agentMigrateTenantOverridesJson ?? "");
setCollectorIdleTimeoutMinutes(adminConfig.collectorIdleTimeoutMinutes ?? 15);
setDesktopDetectorNoCandidateTimeoutMinutes(adminConfig.desktopDetectorNoCandidateTimeoutMinutes ?? 10);
setOpsEventRetentionDays(adminConfig.opsEventRetentionDays ?? 90);
setSlaNotificationCooldownHours(adminConfig.slaNotificationCooldownHours ?? 24);
setAllowAgentDowngrade(adminConfig.allowAgentDowngrade ?? false);
setEnforceClientAppBinding(adminConfig.enforceClientAppBinding ?? false);
setMcpClientRegistrationEnabled(adminConfig.mcpClientRegistrationEnabled ?? false);
setModernDeploymentHarmlessEventIds(parseHarmlessEventIdsJson(adminConfig.modernDeploymentHarmlessEventIdsJson));
setSessionDeletionKillSwitch(adminConfig.sessionDeletionKillSwitch ?? false);
setAutoApproveNewTenants(adminConfig.autoApproveNewTenants ?? false);
setSelfServiceAppHomingEnabled(adminConfig.selfServiceAppHomingEnabled ?? false);
setImeMsiArchivingEnabled(adminConfig.imeMsiArchivingEnabled ?? true);
setMaxImeMsiDownloadSizeMB(adminConfig.maxImeMsiDownloadSizeMB ?? 250);
try {
setGlobalDiagPaths(adminConfig.diagnosticsGlobalLogPathsJson ? JSON.parse(adminConfig.diagnosticsGlobalLogPathsJson) : []);
} catch {
setGlobalDiagPaths([]);
}
try {
setOpsAlertRules(adminConfig.opsAlertRulesJson ? JSON.parse(adminConfig.opsAlertRulesJson) : []);
} catch {
setOpsAlertRules([]);
}
try {
setOpsNotificationChannels(
adminConfig.opsNotificationChannelsJson ? JSON.parse(adminConfig.opsNotificationChannelsJson) : []);
} catch {
setOpsNotificationChannels([]);
}
setOpsAlertTelegramEnabled(adminConfig.opsAlertTelegramEnabled ?? false);
setOpsAlertTelegramChatId(adminConfig.opsAlertTelegramChatId ?? "");
setOpsAlertTeamsEnabled(adminConfig.opsAlertTeamsEnabled ?? false);
setOpsAlertTeamsWebhookUrl(adminConfig.opsAlertTeamsWebhookUrl ?? "");
setOpsAlertSlackEnabled(adminConfig.opsAlertSlackEnabled ?? false);
setOpsAlertSlackWebhookUrl(adminConfig.opsAlertSlackWebhookUrl ?? "");
setExcessiveEventCountThreshold(adminConfig.excessiveEventCountThreshold ?? 2000);
setExcessiveEventAutoActionMode(adminConfig.excessiveEventAutoActionMode ?? "Off");
setExcessiveEventAutoActionThreshold(adminConfig.excessiveEventAutoActionThreshold ?? 2500);
setExcessiveEventAutoActionDurationHours(adminConfig.excessiveEventAutoActionDurationHours ?? 24);
setSuccessMessage(null);
setError(null);
}, [adminConfig]);
// Save diagnostics paths
const handleSaveDiagPaths = useCallback(async (paths: DiagnosticsLogPath[]) => {
@@ -591,7 +538,7 @@ export function AdminConfigProvider({ children }: { children: React.ReactNode })
notificationEmails, setNotificationEmails,
error, setError, successMessage, setSuccessMessage,
getAccessToken,
handleSaveAdminConfig, handleResetAdminConfig, handleSaveDiagPaths, handleSaveOpsAlertConfig,
handleSaveAdminConfig, handleSaveDiagPaths, handleSaveOpsAlertConfig,
opsNotificationChannels, setOpsNotificationChannels,
handleTestOpsChannel, testingOpsChannelId, testOpsChannelResult,
}}>
@@ -46,7 +46,6 @@ interface AdminConfigSettingsSectionProps {
maxImeMsiDownloadSizeMB: number;
setMaxImeMsiDownloadSizeMB: (value: number) => void;
onSave: () => Promise<void>;
onReset: () => void;
}
export function AdminConfigSettingsSection({
@@ -92,7 +91,6 @@ export function AdminConfigSettingsSection({
maxImeMsiDownloadSizeMB,
setMaxImeMsiDownloadSizeMB,
onSave,
onReset,
}: AdminConfigSettingsSectionProps) {
return (
<div className="bg-gradient-to-br from-indigo-50 to-blue-50 dark:from-gray-800 dark:to-gray-800 border-2 border-indigo-300 dark:border-indigo-700 rounded-lg shadow-lg">
@@ -492,13 +490,6 @@ export function AdminConfigSettingsSection({
)}
<div className="flex items-center justify-end space-x-3 pt-2">
<button
onClick={onReset}
disabled={savingConfig}
className="px-5 py-2 border border-indigo-300 dark:border-indigo-600 rounded-md text-indigo-800 dark:text-indigo-200 bg-white dark:bg-gray-700 hover:bg-indigo-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Reset
</button>
<button
onClick={onSave}
disabled={savingConfig}
@@ -3,7 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import { api } from "@/lib/api";
import { apiErrorText, fetchJson } from "@/lib/apiClient";
import SaveResetBar from "../../settings/components/SaveResetBar";
import SaveBar from "../../settings/components/SaveBar";
import VulnerabilityExposurePanel from "@/components/VulnerabilityExposurePanel";
import { fromWireAdminConfiguration, type AdminConfiguration } from "@/types/adminConfig";
import { saveAdminConfigChanges } from "@/lib/adminConfigSave";
@@ -185,17 +185,12 @@ export function VulnerabilityDataSection({
} catch (err) {
const message = apiErrorText(err, "Failed to save vulnerability configuration");
setError(message);
throw err; // Let SaveResetBar show "Save failed"
throw err; // Let SaveBar show "Save failed"
} finally {
setSaving(false);
}
}, [adminConfig, nvdApiKey, vulnerabilityCorrelationEnabled, getAccessToken, setError, setSuccessMessage, onAdminConfigUpdated]);
const handleReset = useCallback(() => {
setNvdApiKey(adminConfig?.nvdApiKey ?? "");
setVulnerabilityCorrelationEnabled(adminConfig?.vulnerabilityCorrelationEnabled ?? true);
setSyncResult(null);
}, [adminConfig]);
const handleSync = useCallback(async () => {
setSyncing(true);
@@ -665,10 +660,9 @@ export function VulnerabilityDataSection({
</p>
</div>
{/* Save / Reset Bar */}
<SaveResetBar
{/* Save Bar */}
<SaveBar
onSave={handleSave}
onReset={handleReset}
saving={saving}
canSave={canMutate}
/>
@@ -28,7 +28,7 @@ export function SectionGlobalSettings() {
selfServiceAppHomingEnabled, setSelfServiceAppHomingEnabled,
imeMsiArchivingEnabled, setImeMsiArchivingEnabled,
maxImeMsiDownloadSizeMB, setMaxImeMsiDownloadSizeMB,
handleSaveAdminConfig, handleResetAdminConfig,
handleSaveAdminConfig,
} = useAdminConfig();
useEffect(() => { ensureAdminConfigLoaded(); }, [ensureAdminConfigLoaded]);
@@ -79,7 +79,6 @@ export function SectionGlobalSettings() {
maxImeMsiDownloadSizeMB={maxImeMsiDownloadSizeMB}
setMaxImeMsiDownloadSizeMB={setMaxImeMsiDownloadSizeMB}
onSave={handleSaveAdminConfig}
onReset={handleResetAdminConfig}
/>
</>
);
@@ -100,7 +100,7 @@ interface TenantConfigContextValue {
loading: boolean;
/**
* True when the current user may CHANGE tenant configuration (Tenant Admin or Global Admin).
* Operators reach the settings area read-only: sections must render no Save/Reset bar and
* Operators reach the settings area read-only: sections must render no Save bar and
* disable their inputs when this is false. Mirrors the backend write gate (PUT config is
* TenantAdminOrGA), so hiding the affordances is UX — the server enforces regardless.
*/
@@ -173,7 +173,6 @@ interface TenantConfigContextValue {
webhookNotifyOnHardwareRejection: boolean;
setWebhookNotifyOnHardwareRejection: (v: boolean) => void;
handleSaveHardwareWhitelist: () => Promise<void>;
handleResetHardwareWhitelist: () => void;
// Agent settings
enablePerformanceCollector: boolean;
@@ -195,7 +194,6 @@ interface TenantConfigContextValue {
companyName: string;
setCompanyName: (v: string) => void;
handleSaveContact: () => Promise<void>;
handleResetContact: () => void;
enableGeoLocation: boolean;
setEnableGeoLocation: (v: boolean) => void;
enableTimezoneAutoSet: boolean;
@@ -225,9 +223,7 @@ interface TenantConfigContextValue {
enableConfigMgrWatcher: boolean;
setEnableConfigMgrWatcher: (v: boolean) => void;
handleSaveAgentParameters: () => Promise<void>;
handleResetAgentParameters: () => void;
handleSaveAgentCollectors: () => Promise<void>;
handleResetAgentCollectors: () => void;
// Agent analyzers
enableLocalAdminAnalyzer: boolean;
@@ -243,7 +239,6 @@ interface TenantConfigContextValue {
enableConsoleBypassDetection: boolean;
setEnableConsoleBypassDetection: (v: boolean) => void;
handleSaveAgentAnalyzers: () => Promise<void>;
handleResetAgentAnalyzers: () => void;
// Unrestricted mode
unrestrictedMode: boolean;
@@ -257,7 +252,6 @@ interface TenantConfigContextValue {
testChannelResult: { channelId: string; success: boolean; message: string } | null;
handleTestChannel: (channelId: string) => Promise<void>;
handleSaveNotifications: () => Promise<void>;
handleResetNotifications: () => void;
// SLA Targets
slaTargetSuccessRate: number | null;
@@ -279,7 +273,6 @@ interface TenantConfigContextValue {
slaConsecutiveFailureThreshold: number;
setSlaConsecutiveFailureThreshold: (v: number) => void;
handleSaveSlaTargets: () => Promise<void>;
handleResetSlaTargets: () => void;
// Diagnostics
diagnosticsBlobSasUrl: string;
@@ -295,7 +288,6 @@ interface TenantConfigContextValue {
newDiagDesc: string;
setNewDiagDesc: (v: string) => void;
handleSaveDiagnostics: () => Promise<void>;
handleResetDiagnostics: () => void;
// Admin management
admins: TenantAdmin[];
@@ -328,7 +320,6 @@ interface TenantConfigContextValue {
sessionTimeoutHours: number;
setSessionTimeoutHours: (v: number) => void;
handleSaveDataManagement: () => Promise<void>;
handleResetDataManagement: () => void;
// Offboarding
showOffboardDialog: boolean;
@@ -369,7 +360,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
const { addNotification } = useNotifications();
// Operators reach the settings area read-only — only a Tenant Admin or Global Admin may
// change configuration. Drives section rendering (no Save/Reset bar, disabled inputs)
// change configuration. Drives section rendering (no Save bar, disabled inputs)
// and the defensive guard in saveConfiguration; the backend enforces the same gate.
const canEditConfig = user?.isTenantAdmin === true || user?.isGlobalAdmin === true;
@@ -880,7 +871,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
[runSectionSave],
);
// Save bars: a failure throws, so SaveResetBar shows its reason next to the button.
// Save bars: a failure throws, so SaveBar shows its reason next to the button.
const saveSection = useCallback(async (sectionName: SettingsSectionName) => {
const failure = await runSectionSave(sectionName);
if (failure !== null) throw new Error(failure);
@@ -1196,22 +1187,9 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}, [tenantId, getAccessToken]);
// -----------------------------------------------------------------------
// Per-section save/reset handlers
// Per-section save handlers
// -----------------------------------------------------------------------
const handleSaveHardwareWhitelist = useCallback(() => saveSection("hardwareWhitelist"), [saveSection]);
const handleResetHardwareWhitelist = useCallback(() => {
if (!config) return;
setManufacturerWhitelist(config.manufacturerWhitelist);
setModelWhitelist(config.modelWhitelist);
setWebhookNotifyOnHardwareRejection(config.webhookNotifyOnHardwareRejection ?? false);
// The hardware-rejection toggle writes through to the per-channel flags — restore those
// to their saved values too, without disturbing other unsaved channel edits.
const savedById = new Map(channelsFromConfig(config).map((c) => [c.id, c]));
setNotificationChannels((prev) => prev.map((c) => {
const saved = savedById.get(c.id);
return saved ? { ...c, notifyOnHardwareRejection: saved.notifyOnHardwareRejection } : c;
}));
}, [config]);
// Hardware-rejection notify: with channels configured, the hardware-section toggle is a
// convenience view over the per-channel flags (checked = any enabled channel opted in;
@@ -1226,96 +1204,20 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}, []);
const handleSaveAgentParameters = useCallback(() => saveSection("agentParameters"), [saveSection]);
const handleResetAgentParameters = useCallback(() => {
if (!config) return;
setSelfDestructOnComplete(config.selfDestructOnComplete ?? true);
setKeepLogFile(config.keepLogFile ?? false);
setRebootOnComplete(config.rebootOnComplete ?? false);
setRebootDelaySeconds(config.rebootDelaySeconds ?? 10);
// contactEmail/companyName deliberately NOT reset here — they are owned by the Contact section
// (resetting it from the Agent Settings card silently discarded unsaved Contact edits).
setEnableGeoLocation(config.enableGeoLocation ?? true);
setEnableTimezoneAutoSet(config.enableTimezoneAutoSet ?? false);
setEnableDoGroupIdAutoSet(config.enableDoGroupIdAutoSet ?? false);
setEnableImeMatchLog(config.enableImeMatchLog ?? false);
setEnableGatherRuleDebugLog(config.enableGatherRuleDebugLog ?? false);
setLogLevel(config.logLevel ?? "Info");
setShowScriptOutput(config.showScriptOutput ?? true);
setShowEnrollmentSummary(config.showEnrollmentSummary ?? false);
setEnrollmentSummaryTimeoutSeconds(config.enrollmentSummaryTimeoutSeconds ?? 60);
setEnrollmentSummaryBrandingImageUrl(config.enrollmentSummaryBrandingImageUrl ?? "");
setEnrollmentSummaryLaunchRetrySeconds(config.enrollmentSummaryLaunchRetrySeconds ?? 120);
setKeepAwakeDuringUserEsp(config.keepAwakeDuringUserEsp ?? false);
}, [config]);
const handleSaveAgentCollectors = useCallback(() => saveSection("agentCollectors"), [saveSection]);
const handleResetAgentCollectors = useCallback(() => {
if (!config) return;
setEnablePerformanceCollector(config.enablePerformanceCollector ?? true);
setPerformanceCollectorInterval(config.performanceCollectorIntervalSeconds ?? 30);
setHelloWaitTimeoutSeconds(config.helloWaitTimeoutSeconds ?? HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS);
setEnableRealmJoinWatcher(config.enableRealmJoinWatcher ?? false);
setEnableConfigMgrWatcher(config.enableConfigMgrWatcher ?? false);
}, [config]);
const handleSaveAgentAnalyzers = useCallback(() => saveSection("agentAnalyzers"), [saveSection]);
const handleResetAgentAnalyzers = useCallback(() => {
if (!config) return;
setEnableLocalAdminAnalyzer(config.enableLocalAdminAnalyzer ?? true);
try {
setLocalAdminAllowedAccounts(config.localAdminAllowedAccountsJson ? JSON.parse(config.localAdminAllowedAccountsJson) : []);
} catch { setLocalAdminAllowedAccounts([]); }
setNewAllowedAccount("");
setEnableSoftwareInventoryAnalyzer(config.enableSoftwareInventoryAnalyzer ?? false);
setEnableIntegrityBypassAnalyzer(config.enableIntegrityBypassAnalyzer ?? true);
setEnableConsoleBypassDetection(config.enableConsoleBypassDetection ?? true);
}, [config]);
const handleSaveNotifications = useCallback(() => saveSection("notifications"), [saveSection]);
const handleResetNotifications = useCallback(() => {
if (!config) return;
setNotificationChannels(channelsFromConfig(config));
setTestChannelResult(null);
}, [config]);
const handleSaveSlaTargets = useCallback(() => saveSection("slaTargets"), [saveSection]);
const handleResetSlaTargets = useCallback(() => {
if (!config) return;
setSlaTargetSuccessRate(config.slaTargetSuccessRate ?? null);
setSlaTargetMaxDurationMinutes(config.slaTargetMaxDurationMinutes ?? null);
setSlaTargetAppInstallSuccessRate(config.slaTargetAppInstallSuccessRate ?? null);
setSlaNotifyOnSuccessRateBreach(config.slaNotifyOnSuccessRateBreach ?? false);
setSlaSuccessRateNotifyThreshold(config.slaSuccessRateNotifyThreshold ?? null);
setSlaNotifyOnDurationBreach(config.slaNotifyOnDurationBreach ?? false);
setSlaNotifyOnAppInstallBreach(config.slaNotifyOnAppInstallBreach ?? false);
setSlaNotifyOnConsecutiveFailures(config.slaNotifyOnConsecutiveFailures ?? false);
setSlaConsecutiveFailureThreshold(config.slaConsecutiveFailureThreshold ?? 5);
}, [config]);
const handleSaveContact = useCallback(() => saveSection("contact"), [saveSection]);
const handleResetContact = useCallback(() => {
if (!config) return;
setContactEmail(config.contactEmail ?? "");
setCompanyName(config.companyName ?? "");
}, [config]);
const handleSaveDiagnostics = useCallback(() => saveSection("diagnostics"), [saveSection]);
const handleResetDiagnostics = useCallback(() => {
if (!config) return;
setDiagnosticsBlobSasUrl(config.diagnosticsBlobSasUrl ?? "");
setDiagnosticsUploadMode(config.diagnosticsUploadMode ?? "Off");
setDiagnosticsUploadDestination(config.diagnosticsUploadDestination ?? "CustomerSas");
try {
setTenantDiagPaths(config.diagnosticsLogPathsJson ? JSON.parse(config.diagnosticsLogPathsJson) : []);
} catch { setTenantDiagPaths([]); }
}, [config]);
const handleSaveDataManagement = useCallback(() => saveSection("dataManagement"), [saveSection]);
const handleResetDataManagement = useCallback(() => {
if (!config) return;
setDataRetentionDays(config.dataRetentionDays ?? 90);
setSessionTimeoutHours(config.sessionTimeoutHours ?? 5);
}, [config]);
// Persist the toggle with the new value passed explicitly. The on/off value MUST be threaded
// through as an override rather than read from `unrestrictedMode` state: the toggle calls this
@@ -1611,7 +1513,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
modelWhitelist, setModelWhitelist,
webhookNotifyOnHardwareRejection: effectiveHwRejectionNotify,
setWebhookNotifyOnHardwareRejection: setHwRejectionNotifyWriteThrough,
handleSaveHardwareWhitelist, handleResetHardwareWhitelist,
handleSaveHardwareWhitelist,
// Agent settings
enablePerformanceCollector, setEnablePerformanceCollector,
@@ -1623,7 +1525,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
rebootDelaySeconds, setRebootDelaySeconds,
contactEmail, setContactEmail,
companyName, setCompanyName,
handleSaveContact, handleResetContact,
handleSaveContact,
enableGeoLocation, setEnableGeoLocation,
enableTimezoneAutoSet, setEnableTimezoneAutoSet,
enableDoGroupIdAutoSet, setEnableDoGroupIdAutoSet,
@@ -1638,8 +1540,8 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
enableRealmJoinWatcher, setEnableRealmJoinWatcher,
enableConfigMgrWatcher, setEnableConfigMgrWatcher,
keepAwakeDuringUserEsp, setKeepAwakeDuringUserEsp,
handleSaveAgentParameters, handleResetAgentParameters,
handleSaveAgentCollectors, handleResetAgentCollectors,
handleSaveAgentParameters,
handleSaveAgentCollectors,
// Agent analyzers
enableLocalAdminAnalyzer, setEnableLocalAdminAnalyzer,
@@ -1648,7 +1550,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer,
enableConsoleBypassDetection, setEnableConsoleBypassDetection,
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
handleSaveAgentAnalyzers,
// Unrestricted mode
unrestrictedMode, setUnrestrictedMode,
@@ -1657,7 +1559,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
// Notifications
notificationChannels, setNotificationChannels,
testingChannelId, testChannelResult,
handleTestChannel, handleSaveNotifications, handleResetNotifications,
handleTestChannel, handleSaveNotifications,
// SLA Targets
slaTargetSuccessRate, setSlaTargetSuccessRate,
@@ -1669,7 +1571,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
slaNotifyOnAppInstallBreach, setSlaNotifyOnAppInstallBreach,
slaNotifyOnConsecutiveFailures, setSlaNotifyOnConsecutiveFailures,
slaConsecutiveFailureThreshold, setSlaConsecutiveFailureThreshold,
handleSaveSlaTargets, handleResetSlaTargets,
handleSaveSlaTargets,
// Diagnostics
diagnosticsBlobSasUrl, setDiagnosticsBlobSasUrl,
@@ -1678,7 +1580,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
tenantDiagPaths, setTenantDiagPaths,
newDiagPath, setNewDiagPath,
newDiagDesc, setNewDiagDesc,
handleSaveDiagnostics, handleResetDiagnostics,
handleSaveDiagnostics,
// Admin management
admins, loadingAdmins,
@@ -1695,7 +1597,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
// Data management
dataRetentionDays, setDataRetentionDays,
sessionTimeoutHours, setSessionTimeoutHours,
handleSaveDataManagement, handleResetDataManagement,
handleSaveDataManagement,
// Offboarding
showOffboardDialog, setShowOffboardDialog,
@@ -1714,33 +1616,33 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
validateIntuneDeviceBinding, handleToggleIntuneDeviceBinding,
saveValidationGate, autopilotConsentInProgress, beginDeviceValidationConsentFlow, detectExistingAccess,
manufacturerWhitelist, modelWhitelist, effectiveHwRejectionNotify, setHwRejectionNotifyWriteThrough,
handleSaveHardwareWhitelist, handleResetHardwareWhitelist,
handleSaveHardwareWhitelist,
enablePerformanceCollector, performanceCollectorInterval, helloWaitTimeoutSeconds,
selfDestructOnComplete, keepLogFile, rebootOnComplete, rebootDelaySeconds,
contactEmail, companyName, handleSaveContact, handleResetContact,
contactEmail, companyName, handleSaveContact,
enableGeoLocation, enableTimezoneAutoSet, enableDoGroupIdAutoSet, enableImeMatchLog, enableGatherRuleDebugLog,
logLevel, showScriptOutput, showEnrollmentSummary, enrollmentSummaryTimeoutSeconds,
enrollmentSummaryBrandingImageUrl, enrollmentSummaryLaunchRetrySeconds,
enableRealmJoinWatcher, enableConfigMgrWatcher, keepAwakeDuringUserEsp,
handleSaveAgentParameters, handleResetAgentParameters,
handleSaveAgentCollectors, handleResetAgentCollectors,
handleSaveAgentParameters,
handleSaveAgentCollectors,
enableLocalAdminAnalyzer, localAdminAllowedAccounts, newAllowedAccount,
enableSoftwareInventoryAnalyzer, enableIntegrityBypassAnalyzer, enableConsoleBypassDetection,
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
handleSaveAgentAnalyzers,
unrestrictedMode, handleSaveUnrestrictedMode,
notificationChannels, testingChannelId, testChannelResult,
handleTestChannel, handleSaveNotifications, handleResetNotifications,
handleTestChannel, handleSaveNotifications,
slaTargetSuccessRate, slaTargetMaxDurationMinutes, slaTargetAppInstallSuccessRate,
slaNotifyOnSuccessRateBreach, slaSuccessRateNotifyThreshold, slaNotifyOnDurationBreach,
slaNotifyOnAppInstallBreach, slaNotifyOnConsecutiveFailures, slaConsecutiveFailureThreshold,
handleSaveSlaTargets, handleResetSlaTargets,
handleSaveSlaTargets,
diagnosticsBlobSasUrl, diagnosticsUploadMode, diagnosticsUploadDestination,
tenantDiagPaths, newDiagPath, newDiagDesc,
handleSaveDiagnostics, handleResetDiagnostics,
handleSaveDiagnostics,
admins, loadingAdmins, newAdminEmail, newMemberRole, newMemberKind, addingAdmin, removingAdmin, togglingAdmin,
handleAddAdmin, handleRemoveAdmin, handleToggleTenantAdmin, handleUpdatePermissions,
bootstrapSessions, bootstrapLoading, fetchBootstrapSessions, createBootstrapSession, revokeBootstrapSession,
dataRetentionDays, sessionTimeoutHours, handleSaveDataManagement, handleResetDataManagement,
dataRetentionDays, sessionTimeoutHours, handleSaveDataManagement,
showOffboardDialog, offboardConfirmText, offboarding, offboardError, handleOffboard,
offboardingInProgress, handleDrainBarrierElapsed,
user, getAccessToken,
@@ -13,7 +13,7 @@ export function SectionAgentAnalyzers() {
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer,
enableConsoleBypassDetection, setEnableConsoleBypassDetection,
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
handleSaveAgentAnalyzers,
savingSection,
} = useTenantConfig();
@@ -34,7 +34,6 @@ export function SectionAgentAnalyzers() {
enableConsoleBypassDetection={enableConsoleBypassDetection}
setEnableConsoleBypassDetection={setEnableConsoleBypassDetection}
onSave={handleSaveAgentAnalyzers}
onReset={handleResetAgentAnalyzers}
saving={savingSection === "agentAnalyzers"}
readOnly={!canEditConfig}
/>
@@ -29,8 +29,8 @@ export function SectionAgentSettings() {
enrollmentSummaryTimeoutSeconds, setEnrollmentSummaryTimeoutSeconds,
enrollmentSummaryBrandingImageUrl, setEnrollmentSummaryBrandingImageUrl,
enrollmentSummaryLaunchRetrySeconds, setEnrollmentSummaryLaunchRetrySeconds,
handleSaveAgentParameters, handleResetAgentParameters,
handleSaveAgentCollectors, handleResetAgentCollectors,
handleSaveAgentParameters,
handleSaveAgentCollectors,
savingSection,
} = useTenantConfig();
@@ -71,7 +71,6 @@ export function SectionAgentSettings() {
enrollmentSummaryLaunchRetrySeconds={enrollmentSummaryLaunchRetrySeconds}
setEnrollmentSummaryLaunchRetrySeconds={setEnrollmentSummaryLaunchRetrySeconds}
onSave={handleSaveAgentParameters}
onReset={handleResetAgentParameters}
saving={savingSection === "agentParameters"}
readOnly={!canEditConfig}
/>
@@ -87,7 +86,6 @@ export function SectionAgentSettings() {
enableConfigMgrWatcher={enableConfigMgrWatcher}
setEnableConfigMgrWatcher={setEnableConfigMgrWatcher}
onSave={handleSaveAgentCollectors}
onReset={handleResetAgentCollectors}
saving={savingSection === "agentCollectors"}
readOnly={!canEditConfig}
/>
@@ -16,7 +16,7 @@ export function SectionDiagnostics() {
newDiagPath, setNewDiagPath,
newDiagDesc, setNewDiagDesc,
unrestrictedMode,
handleSaveDiagnostics, handleResetDiagnostics,
handleSaveDiagnostics,
savingSection,
} = useTenantConfig();
// Built-in sections + platform-wide paths (MemberRead) — read-only context for every role.
@@ -46,7 +46,6 @@ export function SectionDiagnostics() {
setNewDiagDesc={setNewDiagDesc}
unrestrictedMode={unrestrictedMode}
onSave={handleSaveDiagnostics}
onReset={handleResetDiagnostics}
saving={savingSection === "diagnostics"}
readOnly={!canEditConfig}
/>
@@ -1,6 +1,6 @@
"use client";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
@@ -19,9 +19,8 @@ interface AgentAnalyzersSectionProps {
enableConsoleBypassDetection: boolean;
setEnableConsoleBypassDetection: (value: boolean) => void;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): analyzers visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): analyzers visible but inert, no Save bar. */
readOnly?: boolean;
}
@@ -53,7 +52,6 @@ export default function AgentAnalyzersSection({
enableConsoleBypassDetection,
setEnableConsoleBypassDetection,
onSave,
onReset,
saving,
readOnly = false,
}: AgentAnalyzersSectionProps) {
@@ -274,8 +272,8 @@ export default function AgentAnalyzersSection({
</div>
</ReadOnlyFieldset>
{/* Save / Reset */}
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{/* Save */}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -1,6 +1,6 @@
"use client";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
@@ -23,9 +23,8 @@ interface AgentCollectorsSectionProps {
enableConfigMgrWatcher: boolean;
setEnableConfigMgrWatcher: (value: boolean) => void;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): settings visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): settings visible but inert, no Save bar. */
readOnly?: boolean;
}
@@ -41,7 +40,6 @@ export default function AgentCollectorsSection({
enableConfigMgrWatcher,
setEnableConfigMgrWatcher,
onSave,
onReset,
saving,
readOnly = false,
}: AgentCollectorsSectionProps) {
@@ -149,7 +147,7 @@ export default function AgentCollectorsSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -1,6 +1,6 @@
"use client";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
@@ -39,9 +39,8 @@ interface AgentParametersSectionProps {
enrollmentSummaryLaunchRetrySeconds: number;
setEnrollmentSummaryLaunchRetrySeconds: (value: number) => void;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): settings visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): settings visible but inert, no Save bar. */
readOnly?: boolean;
}
@@ -79,7 +78,6 @@ export default function AgentParametersSection({
enrollmentSummaryLaunchRetrySeconds,
setEnrollmentSummaryLaunchRetrySeconds,
onSave,
onReset,
saving,
readOnly = false,
}: AgentParametersSectionProps) {
@@ -337,7 +335,7 @@ export default function AgentParametersSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -1,6 +1,6 @@
"use client";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
@@ -14,9 +14,8 @@ interface DataManagementSectionProps {
/** Edition retention cap (Community 90 / Pro 365) — from feature-flags entitlements. */
retentionCapDays?: number;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): values visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): values visible but inert, no Save bar. */
readOnly?: boolean;
}
@@ -28,7 +27,6 @@ export default function DataManagementSection({
isGlobalAdmin,
retentionCapDays = 90,
onSave,
onReset,
saving,
readOnly = false,
}: DataManagementSectionProps) {
@@ -125,7 +123,7 @@ export default function DataManagementSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -7,7 +7,7 @@ import { validateDiagnosticsPath } from "@/lib/guardValidation";
import { ValidationIndicator } from "@/components/ValidationIndicator";
import { BuiltInSectionsList } from "@/components/diagnostics/BuiltInSectionsList";
import { ContextPill, DiagnosticsPathRow } from "@/components/diagnostics/DiagnosticsPathRow";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { parseSasExpiry } from "./diagnosticsSasExpiry";
import { SectionCardHeader } from "@/components/SectionCardHeader";
@@ -39,9 +39,8 @@ interface DiagnosticsSectionProps {
setNewDiagDesc: (value: string) => void;
unrestrictedMode?: boolean;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): settings visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): settings visible but inert, no Save bar. */
readOnly?: boolean;
}
@@ -64,7 +63,6 @@ export default function DiagnosticsSection({
setNewDiagDesc,
unrestrictedMode = false,
onSave,
onReset,
saving,
readOnly = false,
}: DiagnosticsSectionProps) {
@@ -368,7 +366,7 @@ export default function DiagnosticsSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { joinList, parseList } from "../lib/hardwareWhitelist";
import { SectionCardHeader } from "@/components/SectionCardHeader";
@@ -13,9 +13,8 @@ interface HardwareWhitelistSectionProps {
modelWhitelist: string;
setModelWhitelist: (value: string) => void;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): inputs disabled, no Save/Reset bar. */
/** Read-only viewer (Operator): inputs disabled, no Save bar. */
readOnly?: boolean;
}
@@ -129,7 +128,6 @@ export default function HardwareWhitelistSection({
modelWhitelist,
setModelWhitelist,
onSave,
onReset,
saving,
readOnly = false,
}: HardwareWhitelistSectionProps) {
@@ -188,7 +186,7 @@ export default function HardwareWhitelistSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -1,6 +1,6 @@
"use client";
import SaveResetBar from "./SaveResetBar";
import SaveBar from "./SaveBar";
import ReadOnlyFieldset from "./ReadOnlyFieldset";
import { MAX_NOTIFICATION_CHANNELS, NotificationChannel } from "../types";
import { ChannelEditor } from "@/components/notifications/ChannelEditor";
@@ -16,9 +16,8 @@ interface NotificationsSectionProps {
testingChannelId: string | null;
testChannelResult: { channelId: string; success: boolean; message: string } | null;
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** Read-only viewer (Operator): channels visible but inert, no Save/Reset bar. */
/** Read-only viewer (Operator): channels visible but inert, no Save bar. */
readOnly?: boolean;
/** Global Admin: the platform-bot Telegram provider is offered in the provider dropdown. */
showTelegramProvider?: boolean;
@@ -34,7 +33,6 @@ export default function NotificationsSection({
testingChannelId,
testChannelResult,
onSave,
onReset,
saving,
readOnly = false,
showTelegramProvider = false,
@@ -103,7 +101,7 @@ export default function NotificationsSection({
</div>
</ReadOnlyFieldset>
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} saving={saving} />}
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
</div>
</div>
);
@@ -4,7 +4,7 @@
* Wraps a settings section's interactive content for read-only viewers (Operators).
* The native fieldset `disabled` attribute inertly disables every nested input, select,
* textarea and button while values stay visible and copyable; a small notice explains why.
* Sections must ADDITIONALLY not render their SaveResetBar when read-only — a read-only
* Sections must ADDITIONALLY not render their SaveBar when read-only — a read-only
* viewer gets no save affordance at all, not a disabled one.
*/
export default function ReadOnlyFieldset({
@@ -3,18 +3,17 @@
import { useState, useCallback, useRef } from "react";
import { apiErrorText } from "@/lib/apiClient";
interface SaveResetBarProps {
interface SaveBarProps {
/** A failed save throws: the bar shows the reason next to the button, where the user is looking. */
onSave: () => Promise<void> | void;
onReset: () => void;
saving: boolean;
/** When false, Save/Reset are disabled (read-only viewer). Defaults to true. */
/** When false, Save is disabled (read-only viewer). Defaults to true. */
canSave?: boolean;
/** Input the user can still fix: Save is disabled with this reason, Reset stays available. */
/** Input the user can still fix: Save is disabled with this reason. */
saveBlockedReason?: string;
}
export default function SaveResetBar({ onSave, onReset, saving, canSave = true, saveBlockedReason }: SaveResetBarProps) {
export default function SaveBar({ onSave, saving, canSave = true, saveBlockedReason }: SaveBarProps) {
const [saveResult, setSaveResult] = useState<"idle" | "saved" | "error">("idle");
const [failureReason, setFailureReason] = useState("");
const savedTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
@@ -27,18 +26,12 @@ export default function SaveResetBar({ onSave, onReset, saving, canSave = true,
setSaveResult("saved");
savedTimer.current = setTimeout(() => setSaveResult("idle"), 3000);
} catch (err) {
// Stays until the next Save or Reset: unlike "Saved!" it needs reading and acting on.
// Stays until the next Save: unlike "Saved!" it needs reading and acting on.
setFailureReason(apiErrorText(err, ""));
setSaveResult("error");
}
}, [onSave]);
const handleReset = useCallback(() => {
clearTimeout(savedTimer.current);
setSaveResult("idle");
onReset();
}, [onReset]);
return (
<div className="flex flex-wrap items-center justify-end gap-3 pt-4 border-t border-gray-200">
{!canSave && (
@@ -56,13 +49,6 @@ export default function SaveResetBar({ onSave, onReset, saving, canSave = true,
<span className="min-w-0 break-words">Save failed{failureReason && `: ${failureReason}`}</span>
</span>
)}
<button
onClick={handleReset}
disabled={saving || !canSave}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
>
Reset
</button>
<button
onClick={handleSave}
disabled={saving || !canSave || !!saveBlockedReason}
@@ -11,7 +11,7 @@ export function SectionDataManagement() {
dataRetentionDays, setDataRetentionDays,
sessionTimeoutHours, setSessionTimeoutHours,
editionInfo,
handleSaveDataManagement, handleResetDataManagement,
handleSaveDataManagement,
savingSection,
} = useTenantConfig();
@@ -31,7 +31,6 @@ export function SectionDataManagement() {
isGlobalAdmin={isGlobalAdminView}
retentionCapDays={editionInfo.entitlements.retentionCapDays}
onSave={handleSaveDataManagement}
onReset={handleResetDataManagement}
saving={savingSection === "dataManagement"}
readOnly={!canEditConfig}
/>
@@ -1,7 +1,7 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import SaveResetBar from "../../components/SaveResetBar";
import SaveBar from "../../components/SaveBar";
import ReadOnlyFieldset from "../../components/ReadOnlyFieldset";
import { SectionCardHeader } from "@/components/SectionCardHeader";
import { DOCS_PATHS } from "@/lib/docsPaths";
@@ -11,7 +11,7 @@ export function SectionContact() {
canEditConfig,
contactEmail, setContactEmail,
companyName, setCompanyName,
handleSaveContact, handleResetContact,
handleSaveContact,
savingSection,
} = useTenantConfig();
@@ -92,9 +92,8 @@ export function SectionContact() {
</div>
{canEditConfig && (
<SaveResetBar
<SaveBar
onSave={handleSaveContact}
onReset={handleResetContact}
saving={savingSection === "contact"}
saveBlockedReason={saveBlockedReason}
/>
@@ -17,7 +17,7 @@ export function SectionHardwareWhitelist() {
modelWhitelist, setModelWhitelist,
webhookNotifyOnHardwareRejection, setWebhookNotifyOnHardwareRejection,
notificationChannels,
handleSaveHardwareWhitelist, handleResetHardwareWhitelist,
handleSaveHardwareWhitelist,
savingSection,
} = useTenantConfig();
@@ -42,7 +42,6 @@ export function SectionHardwareWhitelist() {
modelWhitelist={modelWhitelist}
setModelWhitelist={setModelWhitelist}
onSave={handleSaveHardwareWhitelist}
onReset={handleResetHardwareWhitelist}
saving={savingSection === "hardwareWhitelist"}
readOnly={!canEditConfig}
/>
@@ -19,7 +19,7 @@ export function SectionNotifications() {
canEditConfig,
notificationChannels, setNotificationChannels,
handleTestChannel, testingChannelId, testChannelResult,
handleSaveNotifications, handleResetNotifications,
handleSaveNotifications,
savingSection,
} = useTenantConfig();
@@ -55,7 +55,6 @@ export function SectionNotifications() {
testingChannelId={testingChannelId}
testChannelResult={testChannelResult}
onSave={handleSaveNotifications}
onReset={handleResetNotifications}
saving={savingSection === "notifications"}
readOnly={!canEditConfig}
showTelegramProvider={showTelegramProvider}
@@ -1,7 +1,7 @@
"use client";
import { useTenantConfig } from "../../TenantConfigContext";
import SaveResetBar from "../../components/SaveResetBar";
import SaveBar from "../../components/SaveBar";
import ReadOnlyFieldset from "../../components/ReadOnlyFieldset";
import { trackEvent } from "@/lib/appInsights";
import { SectionCardHeader } from "@/components/SectionCardHeader";
@@ -19,7 +19,7 @@ export function SectionSlaTargets() {
slaNotifyOnAppInstallBreach, setSlaNotifyOnAppInstallBreach,
slaNotifyOnConsecutiveFailures, setSlaNotifyOnConsecutiveFailures,
slaConsecutiveFailureThreshold, setSlaConsecutiveFailureThreshold,
handleSaveSlaTargets, handleResetSlaTargets,
handleSaveSlaTargets,
savingSection,
} = useTenantConfig();
@@ -218,7 +218,7 @@ export function SectionSlaTargets() {
</ReadOnlyFieldset>
{canEditConfig && (
<SaveResetBar
<SaveBar
onSave={() => {
trackEvent('sla_targets_saved', {
hasSuccessRateTarget: slaTargetSuccessRate != null,
@@ -227,10 +227,6 @@ export function SectionSlaTargets() {
});
return handleSaveSlaTargets();
}}
onReset={() => {
trackEvent('sla_targets_reset');
return handleResetSlaTargets();
}}
saving={savingSection === "slaTargets"}
/>
)}