mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-11-01 00:11:38 +00:00
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:
@@ -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({
|
||||
|
||||
+5
-19
@@ -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}
|
||||
+1
-2
@@ -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}
|
||||
/>
|
||||
|
||||
+1
-2
@@ -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"}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user