mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-30 15:35:48 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82c0b20be1 | |||
| 4fa439cc3c | |||
| b631c63c53 | |||
| e9aaf3db53 | |||
| a37930e87a | |||
| 6bc5d47afe | |||
| 0ebbf87e5b |
+1
-1
Submodule .claude/commands updated: c5604145b3...f8872d050f
+1
-1
Submodule internal updated: 2609b9f3e5...1762c631db
+1
-1
@@ -1004,7 +1004,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
var fingerprint = Persistence.StartupEventGate.ComputeFingerprint(data, excludedFields);
|
||||
if (_startupGate.ShouldEmit(eventType, fingerprint)) return true;
|
||||
|
||||
_logger.Debug($"EnrollmentTracker: '{eventType}' suppressed — payload unchanged since last emission (restart dedup)");
|
||||
_logger.Debug($"EnrollmentTracker: '{eventType}' suppressed — payload unchanged since last emission (restart dedup)", repeatKey: "DeviceInfo.unchanged:" + eventType);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic
|
||||
// Suppress events when network hasn't actually changed (DHCP renewals, OS chatter)
|
||||
if (previousState != null && previousState.IsSameNetwork(newState))
|
||||
{
|
||||
_logger.Debug("NetworkChangeDetector: network address changed but same logical network — suppressing");
|
||||
_logger.Debug("NetworkChangeDetector: network address changed but same logical network — suppressing", repeatKey: "NetworkChangeDetector.sameNetwork");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -172,8 +172,7 @@ interface TenantConfigContextValue {
|
||||
setModelWhitelist: (v: string) => void;
|
||||
webhookNotifyOnHardwareRejection: boolean;
|
||||
setWebhookNotifyOnHardwareRejection: (v: boolean) => void;
|
||||
handleSaveHardwareWhitelist: () => void;
|
||||
handleResetHardwareWhitelist: () => void;
|
||||
handleSaveHardwareWhitelist: () => Promise<void>;
|
||||
|
||||
// Agent settings
|
||||
enablePerformanceCollector: boolean;
|
||||
@@ -194,8 +193,7 @@ interface TenantConfigContextValue {
|
||||
setContactEmail: (v: string) => void;
|
||||
companyName: string;
|
||||
setCompanyName: (v: string) => void;
|
||||
handleSaveContact: () => void;
|
||||
handleResetContact: () => void;
|
||||
handleSaveContact: () => Promise<void>;
|
||||
enableGeoLocation: boolean;
|
||||
setEnableGeoLocation: (v: boolean) => void;
|
||||
enableTimezoneAutoSet: boolean;
|
||||
@@ -224,8 +222,8 @@ interface TenantConfigContextValue {
|
||||
setEnableRealmJoinWatcher: (v: boolean) => void;
|
||||
enableConfigMgrWatcher: boolean;
|
||||
setEnableConfigMgrWatcher: (v: boolean) => void;
|
||||
handleSaveAgentSettings: () => void;
|
||||
handleResetAgentSettings: () => void;
|
||||
handleSaveAgentParameters: () => Promise<void>;
|
||||
handleSaveAgentCollectors: () => Promise<void>;
|
||||
|
||||
// Agent analyzers
|
||||
enableLocalAdminAnalyzer: boolean;
|
||||
@@ -240,8 +238,7 @@ interface TenantConfigContextValue {
|
||||
setEnableIntegrityBypassAnalyzer: (v: boolean) => void;
|
||||
enableConsoleBypassDetection: boolean;
|
||||
setEnableConsoleBypassDetection: (v: boolean) => void;
|
||||
handleSaveAgentAnalyzers: () => void;
|
||||
handleResetAgentAnalyzers: () => void;
|
||||
handleSaveAgentAnalyzers: () => Promise<void>;
|
||||
|
||||
// Unrestricted mode
|
||||
unrestrictedMode: boolean;
|
||||
@@ -254,8 +251,7 @@ interface TenantConfigContextValue {
|
||||
testingChannelId: string | null;
|
||||
testChannelResult: { channelId: string; success: boolean; message: string } | null;
|
||||
handleTestChannel: (channelId: string) => Promise<void>;
|
||||
handleSaveNotifications: () => void;
|
||||
handleResetNotifications: () => void;
|
||||
handleSaveNotifications: () => Promise<void>;
|
||||
|
||||
// SLA Targets
|
||||
slaTargetSuccessRate: number | null;
|
||||
@@ -276,8 +272,7 @@ interface TenantConfigContextValue {
|
||||
setSlaNotifyOnConsecutiveFailures: (v: boolean) => void;
|
||||
slaConsecutiveFailureThreshold: number;
|
||||
setSlaConsecutiveFailureThreshold: (v: number) => void;
|
||||
handleSaveSlaTargets: () => void;
|
||||
handleResetSlaTargets: () => void;
|
||||
handleSaveSlaTargets: () => Promise<void>;
|
||||
|
||||
// Diagnostics
|
||||
diagnosticsBlobSasUrl: string;
|
||||
@@ -292,8 +287,7 @@ interface TenantConfigContextValue {
|
||||
setNewDiagPath: (v: string) => void;
|
||||
newDiagDesc: string;
|
||||
setNewDiagDesc: (v: string) => void;
|
||||
handleSaveDiagnostics: () => void;
|
||||
handleResetDiagnostics: () => void;
|
||||
handleSaveDiagnostics: () => Promise<void>;
|
||||
|
||||
// Admin management
|
||||
admins: TenantAdmin[];
|
||||
@@ -325,8 +319,7 @@ interface TenantConfigContextValue {
|
||||
setDataRetentionDays: (v: number) => void;
|
||||
sessionTimeoutHours: number;
|
||||
setSessionTimeoutHours: (v: number) => void;
|
||||
handleSaveDataManagement: () => void;
|
||||
handleResetDataManagement: () => void;
|
||||
handleSaveDataManagement: () => Promise<void>;
|
||||
|
||||
// Offboarding
|
||||
showOffboardDialog: boolean;
|
||||
@@ -367,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;
|
||||
|
||||
@@ -727,10 +720,11 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
// -----------------------------------------------------------------------
|
||||
// Save configuration (shared by all sections)
|
||||
// -----------------------------------------------------------------------
|
||||
const saveConfiguration = useCallback(async (sectionName: SettingsSectionName, overrides?: { validateAutopilotDevice?: boolean; validateCorporateIdentifier?: boolean; validateDeviceAssociation?: boolean; validateCloudPcDevice?: boolean; validateIntuneDeviceBinding?: boolean; unrestrictedMode?: boolean }): Promise<boolean> => {
|
||||
// Resolves to null when the save went through, otherwise to the text of the failure.
|
||||
const runSectionSave = useCallback(async (sectionName: SettingsSectionName, overrides?: { validateAutopilotDevice?: boolean; validateCorporateIdentifier?: boolean; validateDeviceAssociation?: boolean; validateCloudPcDevice?: boolean; validateIntuneDeviceBinding?: boolean; unrestrictedMode?: boolean }): Promise<string | null> => {
|
||||
// Read-only viewers (Operators) have no save affordances; this guard covers any path
|
||||
// that still reaches a save (the backend would 403 the PATCH regardless).
|
||||
if (!tenantId || !config || !canEditConfig) return false;
|
||||
if (!tenantId || !config || !canEditConfig) return "This configuration cannot be edited right now.";
|
||||
|
||||
// The validation gates and Unrestricted Mode have no save bar, so their form state must always
|
||||
// equal the stored row. Some of their toggles move the state before this save runs, so a failed
|
||||
@@ -832,7 +826,9 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
if (await patchTenantConfigFields(api.config.fields(tenantId), patchFields, `settings:${sectionName}`, getAccessToken)) {
|
||||
// The feature flags derive from these fields: the cached copy is stale from here on.
|
||||
invalidateCachedAuthFetch(CONFIG_PATH_PREFIX);
|
||||
setConfig({ ...config, ...patchFields } as TenantConfiguration);
|
||||
// Functional merge: the Agent Settings page has two Save bars, and a save still in flight
|
||||
// must not lose its fields to the later one that started from the same loaded config.
|
||||
setConfig((prev) => ({ ...(prev ?? config), ...patchFields }) as TenantConfiguration);
|
||||
}
|
||||
|
||||
// Sync the gate-relevant form state with what is now persisted (merge of loaded
|
||||
@@ -841,13 +837,13 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
trackEvent("settings_saved", { section: sectionName, fieldCount: Object.keys(patchFields).length });
|
||||
setSuccessMessage("Configuration saved successfully!");
|
||||
setTimeout(() => setSuccessMessage(null), 3000);
|
||||
return true;
|
||||
return null;
|
||||
} catch (err) {
|
||||
syncGateState(config);
|
||||
const msg = apiErrorText(err, "Failed to save configuration");
|
||||
trackEvent("settings_error", { action: "save", section: sectionName, error: msg });
|
||||
setError(msg);
|
||||
return false;
|
||||
return msg;
|
||||
} finally {
|
||||
setSavingSection(null);
|
||||
}
|
||||
@@ -868,6 +864,19 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
enableIntegrityBypassAnalyzer, enableConsoleBypassDetection, unrestrictedMode,
|
||||
]);
|
||||
|
||||
// Flows without a save bar (validation gates, Unrestricted Mode, consent) branch on the outcome.
|
||||
const saveConfiguration = useCallback(
|
||||
async (sectionName: SettingsSectionName, overrides?: Parameters<typeof runSectionSave>[1]) =>
|
||||
(await runSectionSave(sectionName, overrides)) === null,
|
||||
[runSectionSave],
|
||||
);
|
||||
|
||||
// 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);
|
||||
}, [runSectionSave]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Consent flow
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -1178,22 +1187,9 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
}, [tenantId, getAccessToken]);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Per-section save/reset handlers
|
||||
// Per-section save handlers
|
||||
// -----------------------------------------------------------------------
|
||||
const handleSaveHardwareWhitelist = useCallback(() => saveConfiguration("hardwareWhitelist"), [saveConfiguration]);
|
||||
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]);
|
||||
const handleSaveHardwareWhitelist = useCallback(() => saveSection("hardwareWhitelist"), [saveSection]);
|
||||
|
||||
// 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;
|
||||
@@ -1207,92 +1203,21 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
setNotificationChannels((prev) => prev.map((c) => ({ ...c, notifyOnHardwareRejection: v })));
|
||||
}, []);
|
||||
|
||||
const handleSaveAgentSettings = useCallback(() => saveConfiguration("agentSettings"), [saveConfiguration]);
|
||||
const handleResetAgentSettings = useCallback(() => {
|
||||
if (!config) return;
|
||||
setEnablePerformanceCollector(config.enablePerformanceCollector ?? true);
|
||||
setPerformanceCollectorInterval(config.performanceCollectorIntervalSeconds ?? 30);
|
||||
setHelloWaitTimeoutSeconds(config.helloWaitTimeoutSeconds ?? HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS);
|
||||
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);
|
||||
setEnableRealmJoinWatcher(config.enableRealmJoinWatcher ?? false);
|
||||
setEnableConfigMgrWatcher(config.enableConfigMgrWatcher ?? false);
|
||||
setKeepAwakeDuringUserEsp(config.keepAwakeDuringUserEsp ?? false);
|
||||
}, [config]);
|
||||
const handleSaveAgentParameters = useCallback(() => saveSection("agentParameters"), [saveSection]);
|
||||
|
||||
const handleSaveAgentAnalyzers = useCallback(() => saveConfiguration("agentAnalyzers"), [saveConfiguration]);
|
||||
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 handleSaveAgentCollectors = useCallback(() => saveSection("agentCollectors"), [saveSection]);
|
||||
|
||||
const handleSaveNotifications = useCallback(() => saveConfiguration("notifications"), [saveConfiguration]);
|
||||
const handleResetNotifications = useCallback(() => {
|
||||
if (!config) return;
|
||||
setNotificationChannels(channelsFromConfig(config));
|
||||
setTestChannelResult(null);
|
||||
}, [config]);
|
||||
const handleSaveAgentAnalyzers = useCallback(() => saveSection("agentAnalyzers"), [saveSection]);
|
||||
|
||||
const handleSaveSlaTargets = useCallback(() => saveConfiguration("slaTargets"), [saveConfiguration]);
|
||||
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 handleSaveNotifications = useCallback(() => saveSection("notifications"), [saveSection]);
|
||||
|
||||
const handleSaveContact = useCallback(() => saveConfiguration("contact"), [saveConfiguration]);
|
||||
const handleResetContact = useCallback(() => {
|
||||
if (!config) return;
|
||||
setContactEmail(config.contactEmail ?? "");
|
||||
setCompanyName(config.companyName ?? "");
|
||||
}, [config]);
|
||||
const handleSaveSlaTargets = useCallback(() => saveSection("slaTargets"), [saveSection]);
|
||||
|
||||
const handleSaveDiagnostics = useCallback(() => saveConfiguration("diagnostics"), [saveConfiguration]);
|
||||
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 handleSaveContact = useCallback(() => saveSection("contact"), [saveSection]);
|
||||
|
||||
const handleSaveDataManagement = useCallback(() => saveConfiguration("dataManagement"), [saveConfiguration]);
|
||||
const handleResetDataManagement = useCallback(() => {
|
||||
if (!config) return;
|
||||
setDataRetentionDays(config.dataRetentionDays ?? 90);
|
||||
setSessionTimeoutHours(config.sessionTimeoutHours ?? 5);
|
||||
}, [config]);
|
||||
const handleSaveDiagnostics = useCallback(() => saveSection("diagnostics"), [saveSection]);
|
||||
|
||||
const handleSaveDataManagement = useCallback(() => saveSection("dataManagement"), [saveSection]);
|
||||
|
||||
// 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
|
||||
@@ -1588,7 +1513,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
modelWhitelist, setModelWhitelist,
|
||||
webhookNotifyOnHardwareRejection: effectiveHwRejectionNotify,
|
||||
setWebhookNotifyOnHardwareRejection: setHwRejectionNotifyWriteThrough,
|
||||
handleSaveHardwareWhitelist, handleResetHardwareWhitelist,
|
||||
handleSaveHardwareWhitelist,
|
||||
|
||||
// Agent settings
|
||||
enablePerformanceCollector, setEnablePerformanceCollector,
|
||||
@@ -1600,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,
|
||||
@@ -1615,7 +1540,8 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
enableRealmJoinWatcher, setEnableRealmJoinWatcher,
|
||||
enableConfigMgrWatcher, setEnableConfigMgrWatcher,
|
||||
keepAwakeDuringUserEsp, setKeepAwakeDuringUserEsp,
|
||||
handleSaveAgentSettings, handleResetAgentSettings,
|
||||
handleSaveAgentParameters,
|
||||
handleSaveAgentCollectors,
|
||||
|
||||
// Agent analyzers
|
||||
enableLocalAdminAnalyzer, setEnableLocalAdminAnalyzer,
|
||||
@@ -1624,7 +1550,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
enableSoftwareInventoryAnalyzer, setEnableSoftwareInventoryAnalyzer,
|
||||
enableIntegrityBypassAnalyzer, setEnableIntegrityBypassAnalyzer,
|
||||
enableConsoleBypassDetection, setEnableConsoleBypassDetection,
|
||||
handleSaveAgentAnalyzers, handleResetAgentAnalyzers,
|
||||
handleSaveAgentAnalyzers,
|
||||
|
||||
// Unrestricted mode
|
||||
unrestrictedMode, setUnrestrictedMode,
|
||||
@@ -1633,7 +1559,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
// Notifications
|
||||
notificationChannels, setNotificationChannels,
|
||||
testingChannelId, testChannelResult,
|
||||
handleTestChannel, handleSaveNotifications, handleResetNotifications,
|
||||
handleTestChannel, handleSaveNotifications,
|
||||
|
||||
// SLA Targets
|
||||
slaTargetSuccessRate, setSlaTargetSuccessRate,
|
||||
@@ -1645,7 +1571,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
slaNotifyOnAppInstallBreach, setSlaNotifyOnAppInstallBreach,
|
||||
slaNotifyOnConsecutiveFailures, setSlaNotifyOnConsecutiveFailures,
|
||||
slaConsecutiveFailureThreshold, setSlaConsecutiveFailureThreshold,
|
||||
handleSaveSlaTargets, handleResetSlaTargets,
|
||||
handleSaveSlaTargets,
|
||||
|
||||
// Diagnostics
|
||||
diagnosticsBlobSasUrl, setDiagnosticsBlobSasUrl,
|
||||
@@ -1654,7 +1580,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
tenantDiagPaths, setTenantDiagPaths,
|
||||
newDiagPath, setNewDiagPath,
|
||||
newDiagDesc, setNewDiagDesc,
|
||||
handleSaveDiagnostics, handleResetDiagnostics,
|
||||
handleSaveDiagnostics,
|
||||
|
||||
// Admin management
|
||||
admins, loadingAdmins,
|
||||
@@ -1671,7 +1597,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
|
||||
// Data management
|
||||
dataRetentionDays, setDataRetentionDays,
|
||||
sessionTimeoutHours, setSessionTimeoutHours,
|
||||
handleSaveDataManagement, handleResetDataManagement,
|
||||
handleSaveDataManagement,
|
||||
|
||||
// Offboarding
|
||||
showOffboardDialog, setShowOffboardDialog,
|
||||
@@ -1690,32 +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,
|
||||
handleSaveAgentSettings, handleResetAgentSettings,
|
||||
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,
|
||||
|
||||
@@ -32,9 +32,10 @@ const EXPECTED_OWNED_FIELDS = [
|
||||
"slaNotifyOnAppInstallBreach", "slaNotifyOnConsecutiveFailures", "slaConsecutiveFailureThreshold",
|
||||
// contact
|
||||
"contactEmail", "companyName",
|
||||
// agentSettings
|
||||
// agentCollectors
|
||||
"enablePerformanceCollector", "performanceCollectorIntervalSeconds", "helloWaitTimeoutSeconds",
|
||||
"enableRealmJoinWatcher", "enableConfigMgrWatcher",
|
||||
// agentParameters
|
||||
"selfDestructOnComplete", "keepLogFile", "rebootOnComplete", "rebootDelaySeconds",
|
||||
"enableGeoLocation", "enableTimezoneAutoSet", "enableDoGroupIdAutoSet", "keepAwakeDuringUserEsp",
|
||||
"enableImeMatchLog", "enableGatherRuleDebugLog",
|
||||
@@ -92,6 +93,15 @@ describe("SECTION_FIELD_MAP parity", () => {
|
||||
expect(owned).toEqual(EXPECTED_OWNED_FIELDS);
|
||||
});
|
||||
|
||||
it("the Agent Collectors card saves only its own fields", () => {
|
||||
// One card, one Save: a field moving back into a shared section would make the Save of one
|
||||
// card write the unsaved edits of the other.
|
||||
expect([...SECTION_FIELD_MAP.agentCollectors.fields].sort()).toEqual([
|
||||
"enableConfigMgrWatcher", "enablePerformanceCollector", "enableRealmJoinWatcher",
|
||||
"helloWaitTimeoutSeconds", "performanceCollectorIntervalSeconds",
|
||||
]);
|
||||
});
|
||||
|
||||
it("alsoWrites entries never overlap the same section's owned fields", () => {
|
||||
for (const [section, spec] of sections) {
|
||||
for (const field of spec.alsoWrites ?? []) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
+22
-16
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import { TenantNotifications } from "../../TenantNotifications";
|
||||
import AgentSettingsSection from "../../components/AgentSettingsSection";
|
||||
import AgentParametersSection from "../../components/AgentParametersSection";
|
||||
import AgentCollectorsSection from "../../components/AgentCollectorsSection";
|
||||
|
||||
export function SectionAgentSettings() {
|
||||
const {
|
||||
@@ -28,24 +29,15 @@ export function SectionAgentSettings() {
|
||||
enrollmentSummaryTimeoutSeconds, setEnrollmentSummaryTimeoutSeconds,
|
||||
enrollmentSummaryBrandingImageUrl, setEnrollmentSummaryBrandingImageUrl,
|
||||
enrollmentSummaryLaunchRetrySeconds, setEnrollmentSummaryLaunchRetrySeconds,
|
||||
handleSaveAgentSettings, handleResetAgentSettings,
|
||||
handleSaveAgentParameters,
|
||||
handleSaveAgentCollectors,
|
||||
savingSection,
|
||||
} = useTenantConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TenantNotifications />
|
||||
<AgentSettingsSection
|
||||
enablePerformanceCollector={enablePerformanceCollector}
|
||||
setEnablePerformanceCollector={setEnablePerformanceCollector}
|
||||
performanceCollectorInterval={performanceCollectorInterval}
|
||||
setPerformanceCollectorInterval={setPerformanceCollectorInterval}
|
||||
helloWaitTimeoutSeconds={helloWaitTimeoutSeconds}
|
||||
setHelloWaitTimeoutSeconds={setHelloWaitTimeoutSeconds}
|
||||
enableRealmJoinWatcher={enableRealmJoinWatcher}
|
||||
setEnableRealmJoinWatcher={setEnableRealmJoinWatcher}
|
||||
enableConfigMgrWatcher={enableConfigMgrWatcher}
|
||||
setEnableConfigMgrWatcher={setEnableConfigMgrWatcher}
|
||||
<AgentParametersSection
|
||||
selfDestructOnComplete={selfDestructOnComplete}
|
||||
setSelfDestructOnComplete={setSelfDestructOnComplete}
|
||||
keepLogFile={keepLogFile}
|
||||
@@ -78,9 +70,23 @@ export function SectionAgentSettings() {
|
||||
setEnrollmentSummaryBrandingImageUrl={setEnrollmentSummaryBrandingImageUrl}
|
||||
enrollmentSummaryLaunchRetrySeconds={enrollmentSummaryLaunchRetrySeconds}
|
||||
setEnrollmentSummaryLaunchRetrySeconds={setEnrollmentSummaryLaunchRetrySeconds}
|
||||
onSave={handleSaveAgentSettings}
|
||||
onReset={handleResetAgentSettings}
|
||||
saving={savingSection === "agentSettings"}
|
||||
onSave={handleSaveAgentParameters}
|
||||
saving={savingSection === "agentParameters"}
|
||||
readOnly={!canEditConfig}
|
||||
/>
|
||||
<AgentCollectorsSection
|
||||
enablePerformanceCollector={enablePerformanceCollector}
|
||||
setEnablePerformanceCollector={setEnablePerformanceCollector}
|
||||
performanceCollectorInterval={performanceCollectorInterval}
|
||||
setPerformanceCollectorInterval={setPerformanceCollectorInterval}
|
||||
helloWaitTimeoutSeconds={helloWaitTimeoutSeconds}
|
||||
setHelloWaitTimeoutSeconds={setHelloWaitTimeoutSeconds}
|
||||
enableRealmJoinWatcher={enableRealmJoinWatcher}
|
||||
setEnableRealmJoinWatcher={setEnableRealmJoinWatcher}
|
||||
enableConfigMgrWatcher={enableConfigMgrWatcher}
|
||||
setEnableConfigMgrWatcher={setEnableConfigMgrWatcher}
|
||||
onSave={handleSaveAgentCollectors}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import SaveBar from "./SaveBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
import {
|
||||
clampHelloWaitTimeoutSeconds,
|
||||
HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS,
|
||||
HELLO_WAIT_TIMEOUT_MAX_SECONDS,
|
||||
HELLO_WAIT_TIMEOUT_MIN_SECONDS,
|
||||
} from "../lib/helloWaitTimeout";
|
||||
|
||||
interface AgentCollectorsSectionProps {
|
||||
enablePerformanceCollector: boolean;
|
||||
setEnablePerformanceCollector: (value: boolean) => void;
|
||||
performanceCollectorInterval: number;
|
||||
setPerformanceCollectorInterval: (value: number) => void;
|
||||
helloWaitTimeoutSeconds: number;
|
||||
setHelloWaitTimeoutSeconds: (value: number) => void;
|
||||
enableRealmJoinWatcher: boolean;
|
||||
setEnableRealmJoinWatcher: (value: boolean) => void;
|
||||
enableConfigMgrWatcher: boolean;
|
||||
setEnableConfigMgrWatcher: (value: boolean) => void;
|
||||
onSave: () => Promise<void> | void;
|
||||
saving: boolean;
|
||||
/** Read-only viewer (Operator): settings visible but inert, no Save bar. */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function AgentCollectorsSection({
|
||||
enablePerformanceCollector,
|
||||
setEnablePerformanceCollector,
|
||||
performanceCollectorInterval,
|
||||
setPerformanceCollectorInterval,
|
||||
helloWaitTimeoutSeconds,
|
||||
setHelloWaitTimeoutSeconds,
|
||||
enableRealmJoinWatcher,
|
||||
setEnableRealmJoinWatcher,
|
||||
enableConfigMgrWatcher,
|
||||
setEnableConfigMgrWatcher,
|
||||
onSave,
|
||||
saving,
|
||||
readOnly = false,
|
||||
}: AgentCollectorsSectionProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<SectionCardHeader
|
||||
tone="emerald"
|
||||
iconPath="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
title="Agent Collectors"
|
||||
subtitle="Enable optional data collectors on enrolled devices. These generate additional telemetry traffic."
|
||||
docsPath={DOCS_PATHS.agentCollectors}
|
||||
/>
|
||||
<div className="p-6 space-y-5">
|
||||
{/* notice={false}: the Agent Parameters card above already shows the read-only notice. */}
|
||||
<ReadOnlyFieldset readOnly={readOnly} notice={false}>
|
||||
<div className="space-y-5">
|
||||
{/* Performance Collector */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="font-medium text-gray-900">Performance Collector</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">CPU, memory, disk metrics at configurable intervals</p>
|
||||
{enablePerformanceCollector && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">Interval:</span>
|
||||
<input
|
||||
type="number"
|
||||
min={30}
|
||||
max={300}
|
||||
value={performanceCollectorInterval}
|
||||
onChange={(e) => setPerformanceCollectorInterval(parseInt(e.target.value) || 30)}
|
||||
onBlur={() => setPerformanceCollectorInterval(Math.max(30, Math.min(300, performanceCollectorInterval)))}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">seconds</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">Minimum: 30 seconds, Maximum: 300 seconds (5 minutes)</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnablePerformanceCollector(!enablePerformanceCollector)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enablePerformanceCollector ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enablePerformanceCollector ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* RealmJoin Watcher */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">RealmJoin Watcher</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Tracks RealmJoin deployment state during provisioning — deployment phase, per-package start and completion, and the RealmJoin completion gate. Off by default; enable only for tenants that provision devices with RealmJoin, elsewhere it produces no signal.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnableRealmJoinWatcher(!enableRealmJoinWatcher)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableRealmJoinWatcher ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableRealmJoinWatcher ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ConfigMgr Watcher */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">ConfigMgr Watcher</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Shows in Session Info whether the Configuration Manager client is installed, with its service state, version and site code. Off by default; enable for tenants that co-manage devices with Configuration Manager.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnableConfigMgrWatcher(!enableConfigMgrWatcher)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableConfigMgrWatcher ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableConfigMgrWatcher ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Hello Wait Timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="font-medium text-gray-900">Hello Wait Timeout</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">Seconds to wait for the Windows Hello wizard after ESP exit</p>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">Timeout:</span>
|
||||
<input
|
||||
type="number"
|
||||
min={HELLO_WAIT_TIMEOUT_MIN_SECONDS}
|
||||
max={HELLO_WAIT_TIMEOUT_MAX_SECONDS}
|
||||
value={helloWaitTimeoutSeconds}
|
||||
onChange={(e) => setHelloWaitTimeoutSeconds(parseInt(e.target.value) || HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS)}
|
||||
onBlur={() => setHelloWaitTimeoutSeconds(clampHelloWaitTimeoutSeconds(helloWaitTimeoutSeconds))}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">seconds</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">Minimum: 30 seconds, Maximum: 3600 seconds (1 hour). Values up to 300 seconds use the built-in 5-minute wait.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ReadOnlyFieldset>
|
||||
|
||||
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import SaveBar from "./SaveBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface AgentParametersSectionProps {
|
||||
selfDestructOnComplete: boolean;
|
||||
setSelfDestructOnComplete: (value: boolean) => void;
|
||||
keepLogFile: boolean;
|
||||
setKeepLogFile: (value: boolean) => void;
|
||||
rebootOnComplete: boolean;
|
||||
setRebootOnComplete: (value: boolean) => void;
|
||||
rebootDelaySeconds: number;
|
||||
setRebootDelaySeconds: (value: number) => void;
|
||||
enableGeoLocation: boolean;
|
||||
setEnableGeoLocation: (value: boolean) => void;
|
||||
enableTimezoneAutoSet: boolean;
|
||||
setEnableTimezoneAutoSet: (value: boolean) => void;
|
||||
enableDoGroupIdAutoSet: boolean;
|
||||
setEnableDoGroupIdAutoSet: (value: boolean) => void;
|
||||
keepAwakeDuringUserEsp: boolean;
|
||||
setKeepAwakeDuringUserEsp: (value: boolean) => void;
|
||||
enableImeMatchLog: boolean;
|
||||
setEnableImeMatchLog: (value: boolean) => void;
|
||||
enableGatherRuleDebugLog: boolean;
|
||||
setEnableGatherRuleDebugLog: (value: boolean) => void;
|
||||
logLevel: string;
|
||||
setLogLevel: (value: string) => void;
|
||||
showScriptOutput: boolean;
|
||||
setShowScriptOutput: (value: boolean) => void;
|
||||
showEnrollmentSummary: boolean;
|
||||
setShowEnrollmentSummary: (value: boolean) => void;
|
||||
enrollmentSummaryTimeoutSeconds: number;
|
||||
setEnrollmentSummaryTimeoutSeconds: (value: number) => void;
|
||||
enrollmentSummaryBrandingImageUrl: string;
|
||||
setEnrollmentSummaryBrandingImageUrl: (value: string) => void;
|
||||
enrollmentSummaryLaunchRetrySeconds: number;
|
||||
setEnrollmentSummaryLaunchRetrySeconds: (value: number) => void;
|
||||
onSave: () => Promise<void> | void;
|
||||
saving: boolean;
|
||||
/** Read-only viewer (Operator): settings visible but inert, no Save bar. */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function AgentParametersSection({
|
||||
selfDestructOnComplete,
|
||||
setSelfDestructOnComplete,
|
||||
keepLogFile,
|
||||
setKeepLogFile,
|
||||
rebootOnComplete,
|
||||
setRebootOnComplete,
|
||||
rebootDelaySeconds,
|
||||
setRebootDelaySeconds,
|
||||
enableGeoLocation,
|
||||
setEnableGeoLocation,
|
||||
enableTimezoneAutoSet,
|
||||
setEnableTimezoneAutoSet,
|
||||
enableDoGroupIdAutoSet,
|
||||
setEnableDoGroupIdAutoSet,
|
||||
keepAwakeDuringUserEsp,
|
||||
setKeepAwakeDuringUserEsp,
|
||||
enableImeMatchLog,
|
||||
setEnableImeMatchLog,
|
||||
enableGatherRuleDebugLog,
|
||||
setEnableGatherRuleDebugLog,
|
||||
logLevel,
|
||||
setLogLevel,
|
||||
showScriptOutput,
|
||||
setShowScriptOutput,
|
||||
showEnrollmentSummary,
|
||||
setShowEnrollmentSummary,
|
||||
enrollmentSummaryTimeoutSeconds,
|
||||
setEnrollmentSummaryTimeoutSeconds,
|
||||
enrollmentSummaryBrandingImageUrl,
|
||||
setEnrollmentSummaryBrandingImageUrl,
|
||||
enrollmentSummaryLaunchRetrySeconds,
|
||||
setEnrollmentSummaryLaunchRetrySeconds,
|
||||
onSave,
|
||||
saving,
|
||||
readOnly = false,
|
||||
}: AgentParametersSectionProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<SectionCardHeader
|
||||
tone="violet"
|
||||
iconPath="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
title="Agent Parameters"
|
||||
subtitle="Control agent behavior on enrolled devices. Changes take effect on the next agent config refresh."
|
||||
docsPath={DOCS_PATHS.agentParameters}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* Self-Destruct */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Self-Destruct on Complete</p>
|
||||
<p className="text-sm text-gray-500">Remove Scheduled Task and all agent files when enrollment completes</p>
|
||||
</div>
|
||||
<button onClick={() => setSelfDestructOnComplete(!selfDestructOnComplete)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${selfDestructOnComplete ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${selfDestructOnComplete ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keep Log File */}
|
||||
{selfDestructOnComplete && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Keep Log File</p>
|
||||
<p className="text-sm text-gray-500">Preserve the agent log during self-destruct (all other files are removed)</p>
|
||||
</div>
|
||||
<button onClick={() => setKeepLogFile(!keepLogFile)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${keepLogFile ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${keepLogFile ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reboot on Complete */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Reboot on Complete</p>
|
||||
<p className="text-sm text-gray-500">Reboot the device after enrollment completes (and after self-destruct if enabled)</p>
|
||||
</div>
|
||||
<button onClick={() => setRebootOnComplete(!rebootOnComplete)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${rebootOnComplete ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${rebootOnComplete ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rebootOnComplete && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Reboot Delay</p>
|
||||
<p className="text-sm text-gray-500">Seconds before reboot is initiated — gives the user time to see what is happening</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={rebootDelaySeconds}
|
||||
onChange={(e) => setRebootDelaySeconds(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Geo Location */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Geo-Location Detection</p>
|
||||
<p className="text-sm text-gray-500">Capture device location, ISP and network info at enrollment start (queries external IP service)</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableGeoLocation(!enableGeoLocation)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableGeoLocation ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableGeoLocation ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Timezone Auto-Set (sub-toggle of Geo-Location) */}
|
||||
{enableGeoLocation && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Set Timezone Automatically</p>
|
||||
<p className="text-sm text-gray-500">Set the device timezone based on IP geolocation result (uses tzutil /s)</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableTimezoneAutoSet(!enableTimezoneAutoSet)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableTimezoneAutoSet ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableTimezoneAutoSet ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delivery Optimization Group ID */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Set Delivery Optimization Group ID</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Set the DOGroupId policy from a network fingerprint — a deterministic GUID derived from the default gateway's IP and MAC address, so devices on the same local network peer with each other. Takes effect with DO Download Mode "Group (2)"; an existing DOGroupId or DOGroupIdSource policy (Intune/GPO) is never overwritten.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableDoGroupIdAutoSet(!enableDoGroupIdAutoSet)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableDoGroupIdAutoSet ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableDoGroupIdAutoSet ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keep Awake During User-ESP */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Keep Awake During User-ESP</p>
|
||||
<p className="text-sm text-gray-500">Hold the device awake (system and display) during the User-ESP (Account Setup) phase so idle standby or sleep cannot stall app installs and account provisioning. Reboots are unaffected; the hold is released once the phase completes.</p>
|
||||
</div>
|
||||
<button onClick={() => setKeepAwakeDuringUserEsp(!keepAwakeDuringUserEsp)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${keepAwakeDuringUserEsp ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${keepAwakeDuringUserEsp ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* IME Match Log */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">IME Pattern Match Log</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Write every matched IME log line to a local file for diagnostics
|
||||
{enableImeMatchLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\ime-pattern-matches.log</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableImeMatchLog(!enableImeMatchLog)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableImeMatchLog ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableImeMatchLog ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Gather Rule Debug Log */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Gather Rule Debug Log</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Trace gather rule evaluation to a local file to diagnose rules that produce no timeline events. The trace never leaves the device.
|
||||
{enableGatherRuleDebugLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\gather-rules-debug.log</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableGatherRuleDebugLog(!enableGatherRuleDebugLog)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableGatherRuleDebugLog ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableGatherRuleDebugLog ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Show Script Output */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">Show Script Output (stdout)</p>
|
||||
<p className="text-sm text-gray-500">Show standard output from PowerShell scripts in the timeline. Disable if scripts may output sensitive data. Error output (stderr) is always shown.</p>
|
||||
</div>
|
||||
<button onClick={() => setShowScriptOutput(!showScriptOutput)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${showScriptOutput ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${showScriptOutput ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log Level */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Log Level</p>
|
||||
<p className="text-sm text-gray-500">Agent log verbosity — Info shows the enrollment flow, Debug adds detail, Verbose adds every detail, Trace adds each internal step (every watcher, poll and queue event)</p>
|
||||
</div>
|
||||
<select
|
||||
value={logLevel}
|
||||
onChange={(e) => setLogLevel(e.target.value)}
|
||||
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
>
|
||||
<option value="Info">Info</option>
|
||||
<option value="Debug">Debug</option>
|
||||
<option value="Verbose">Verbose</option>
|
||||
<option value="Trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Enrollment Summary Dialog */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">Show Enrollment Summary</p>
|
||||
<p className="text-sm text-gray-500">Display a visual enrollment summary dialog to the end user after enrollment completes (success or failure). Requires the SummaryDialog companion to be deployed alongside the agent.</p>
|
||||
</div>
|
||||
<button onClick={() => setShowEnrollmentSummary(!showEnrollmentSummary)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${showEnrollmentSummary ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${showEnrollmentSummary ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showEnrollmentSummary && (
|
||||
<>
|
||||
{/* Auto-close timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Auto-Close Timeout</p>
|
||||
<p className="text-sm text-gray-500">Seconds until the summary dialog closes automatically. 0 = no auto-close.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={enrollmentSummaryTimeoutSeconds}
|
||||
onChange={(e) => setEnrollmentSummaryTimeoutSeconds(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Launch Retry Timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Launch Retry Timeout</p>
|
||||
<p className="text-sm text-gray-500">How long the agent retries launching the dialog when the desktop is locked by a credential UI (e.g. Windows Hello). 0 = no retry.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={enrollmentSummaryLaunchRetrySeconds}
|
||||
onChange={(e) => setEnrollmentSummaryLaunchRetrySeconds(Math.min(3600, Math.max(0, parseInt(e.target.value) || 0)))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branding Image URL */}
|
||||
<div className="p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div className="mb-2">
|
||||
<p className="font-medium text-gray-900">Branding Image URL</p>
|
||||
<p className="text-sm text-gray-500">Optional banner image at the top of the summary dialog. Recommended size: 540 x 80 px. Larger images will be center-cropped.</p>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
value={enrollmentSummaryBrandingImageUrl}
|
||||
onChange={(e) => setEnrollmentSummaryBrandingImageUrl(e.target.value)}
|
||||
placeholder="https://example.com/branding-banner.png"
|
||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ReadOnlyFieldset>
|
||||
|
||||
{!readOnly && <SaveBar onSave={onSave} saving={saving} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
import {
|
||||
clampHelloWaitTimeoutSeconds,
|
||||
HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS,
|
||||
HELLO_WAIT_TIMEOUT_MAX_SECONDS,
|
||||
HELLO_WAIT_TIMEOUT_MIN_SECONDS,
|
||||
} from "../lib/helloWaitTimeout";
|
||||
|
||||
interface AgentSettingsSectionProps {
|
||||
enablePerformanceCollector: boolean;
|
||||
setEnablePerformanceCollector: (value: boolean) => void;
|
||||
performanceCollectorInterval: number;
|
||||
setPerformanceCollectorInterval: (value: number) => void;
|
||||
helloWaitTimeoutSeconds: number;
|
||||
setHelloWaitTimeoutSeconds: (value: number) => void;
|
||||
enableRealmJoinWatcher: boolean;
|
||||
setEnableRealmJoinWatcher: (value: boolean) => void;
|
||||
enableConfigMgrWatcher: boolean;
|
||||
setEnableConfigMgrWatcher: (value: boolean) => void;
|
||||
selfDestructOnComplete: boolean;
|
||||
setSelfDestructOnComplete: (value: boolean) => void;
|
||||
keepLogFile: boolean;
|
||||
setKeepLogFile: (value: boolean) => void;
|
||||
rebootOnComplete: boolean;
|
||||
setRebootOnComplete: (value: boolean) => void;
|
||||
rebootDelaySeconds: number;
|
||||
setRebootDelaySeconds: (value: number) => void;
|
||||
enableGeoLocation: boolean;
|
||||
setEnableGeoLocation: (value: boolean) => void;
|
||||
enableTimezoneAutoSet: boolean;
|
||||
setEnableTimezoneAutoSet: (value: boolean) => void;
|
||||
enableDoGroupIdAutoSet: boolean;
|
||||
setEnableDoGroupIdAutoSet: (value: boolean) => void;
|
||||
keepAwakeDuringUserEsp: boolean;
|
||||
setKeepAwakeDuringUserEsp: (value: boolean) => void;
|
||||
enableImeMatchLog: boolean;
|
||||
setEnableImeMatchLog: (value: boolean) => void;
|
||||
enableGatherRuleDebugLog: boolean;
|
||||
setEnableGatherRuleDebugLog: (value: boolean) => void;
|
||||
logLevel: string;
|
||||
setLogLevel: (value: string) => void;
|
||||
showScriptOutput: boolean;
|
||||
setShowScriptOutput: (value: boolean) => void;
|
||||
showEnrollmentSummary: boolean;
|
||||
setShowEnrollmentSummary: (value: boolean) => void;
|
||||
enrollmentSummaryTimeoutSeconds: number;
|
||||
setEnrollmentSummaryTimeoutSeconds: (value: number) => void;
|
||||
enrollmentSummaryBrandingImageUrl: string;
|
||||
setEnrollmentSummaryBrandingImageUrl: (value: string) => void;
|
||||
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. */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function AgentSettingsSection({
|
||||
enablePerformanceCollector,
|
||||
setEnablePerformanceCollector,
|
||||
performanceCollectorInterval,
|
||||
setPerformanceCollectorInterval,
|
||||
helloWaitTimeoutSeconds,
|
||||
setHelloWaitTimeoutSeconds,
|
||||
enableRealmJoinWatcher,
|
||||
setEnableRealmJoinWatcher,
|
||||
enableConfigMgrWatcher,
|
||||
setEnableConfigMgrWatcher,
|
||||
selfDestructOnComplete,
|
||||
setSelfDestructOnComplete,
|
||||
keepLogFile,
|
||||
setKeepLogFile,
|
||||
rebootOnComplete,
|
||||
setRebootOnComplete,
|
||||
rebootDelaySeconds,
|
||||
setRebootDelaySeconds,
|
||||
enableGeoLocation,
|
||||
setEnableGeoLocation,
|
||||
enableTimezoneAutoSet,
|
||||
setEnableTimezoneAutoSet,
|
||||
enableDoGroupIdAutoSet,
|
||||
setEnableDoGroupIdAutoSet,
|
||||
keepAwakeDuringUserEsp,
|
||||
setKeepAwakeDuringUserEsp,
|
||||
enableImeMatchLog,
|
||||
setEnableImeMatchLog,
|
||||
enableGatherRuleDebugLog,
|
||||
setEnableGatherRuleDebugLog,
|
||||
logLevel,
|
||||
setLogLevel,
|
||||
showScriptOutput,
|
||||
setShowScriptOutput,
|
||||
showEnrollmentSummary,
|
||||
setShowEnrollmentSummary,
|
||||
enrollmentSummaryTimeoutSeconds,
|
||||
setEnrollmentSummaryTimeoutSeconds,
|
||||
enrollmentSummaryBrandingImageUrl,
|
||||
setEnrollmentSummaryBrandingImageUrl,
|
||||
enrollmentSummaryLaunchRetrySeconds,
|
||||
setEnrollmentSummaryLaunchRetrySeconds,
|
||||
onSave,
|
||||
onReset,
|
||||
saving,
|
||||
readOnly = false,
|
||||
}: AgentSettingsSectionProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Agent Parameters */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<SectionCardHeader
|
||||
tone="violet"
|
||||
iconPath="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
title="Agent Parameters"
|
||||
subtitle="Control agent behavior on enrolled devices. Changes take effect on the next agent config refresh."
|
||||
docsPath={DOCS_PATHS.agentParameters}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* Self-Destruct */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Self-Destruct on Complete</p>
|
||||
<p className="text-sm text-gray-500">Remove Scheduled Task and all agent files when enrollment completes</p>
|
||||
</div>
|
||||
<button onClick={() => setSelfDestructOnComplete(!selfDestructOnComplete)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${selfDestructOnComplete ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${selfDestructOnComplete ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keep Log File */}
|
||||
{selfDestructOnComplete && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Keep Log File</p>
|
||||
<p className="text-sm text-gray-500">Preserve the agent log during self-destruct (all other files are removed)</p>
|
||||
</div>
|
||||
<button onClick={() => setKeepLogFile(!keepLogFile)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${keepLogFile ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${keepLogFile ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reboot on Complete */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Reboot on Complete</p>
|
||||
<p className="text-sm text-gray-500">Reboot the device after enrollment completes (and after self-destruct if enabled)</p>
|
||||
</div>
|
||||
<button onClick={() => setRebootOnComplete(!rebootOnComplete)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${rebootOnComplete ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${rebootOnComplete ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rebootOnComplete && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Reboot Delay</p>
|
||||
<p className="text-sm text-gray-500">Seconds before reboot is initiated — gives the user time to see what is happening</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={rebootDelaySeconds}
|
||||
onChange={(e) => setRebootDelaySeconds(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Geo Location */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Geo-Location Detection</p>
|
||||
<p className="text-sm text-gray-500">Capture device location, ISP and network info at enrollment start (queries external IP service)</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableGeoLocation(!enableGeoLocation)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableGeoLocation ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableGeoLocation ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Timezone Auto-Set (sub-toggle of Geo-Location) */}
|
||||
{enableGeoLocation && (
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Set Timezone Automatically</p>
|
||||
<p className="text-sm text-gray-500">Set the device timezone based on IP geolocation result (uses tzutil /s)</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableTimezoneAutoSet(!enableTimezoneAutoSet)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableTimezoneAutoSet ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableTimezoneAutoSet ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delivery Optimization Group ID */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Set Delivery Optimization Group ID</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Set the DOGroupId policy from a network fingerprint — a deterministic GUID derived from the default gateway's IP and MAC address, so devices on the same local network peer with each other. Takes effect with DO Download Mode "Group (2)"; an existing DOGroupId or DOGroupIdSource policy (Intune/GPO) is never overwritten.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableDoGroupIdAutoSet(!enableDoGroupIdAutoSet)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableDoGroupIdAutoSet ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableDoGroupIdAutoSet ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keep Awake During User-ESP */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Keep Awake During User-ESP</p>
|
||||
<p className="text-sm text-gray-500">Hold the device awake (system and display) during the User-ESP (Account Setup) phase so idle standby or sleep cannot stall app installs and account provisioning. Reboots are unaffected; the hold is released once the phase completes.</p>
|
||||
</div>
|
||||
<button onClick={() => setKeepAwakeDuringUserEsp(!keepAwakeDuringUserEsp)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${keepAwakeDuringUserEsp ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${keepAwakeDuringUserEsp ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* IME Match Log */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">IME Pattern Match Log</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Write every matched IME log line to a local file for diagnostics
|
||||
{enableImeMatchLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\ime-pattern-matches.log</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableImeMatchLog(!enableImeMatchLog)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableImeMatchLog ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableImeMatchLog ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Gather Rule Debug Log */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Gather Rule Debug Log</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Trace gather rule evaluation to a local file to diagnose rules that produce no timeline events. The trace never leaves the device.
|
||||
{enableGatherRuleDebugLog && <span className="block text-xs text-gray-400 mt-0.5 font-mono">%ProgramData%\AutopilotMonitor\Logs\gather-rules-debug.log</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setEnableGatherRuleDebugLog(!enableGatherRuleDebugLog)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableGatherRuleDebugLog ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableGatherRuleDebugLog ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Show Script Output */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">Show Script Output (stdout)</p>
|
||||
<p className="text-sm text-gray-500">Show standard output from PowerShell scripts in the timeline. Disable if scripts may output sensitive data. Error output (stderr) is always shown.</p>
|
||||
</div>
|
||||
<button onClick={() => setShowScriptOutput(!showScriptOutput)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${showScriptOutput ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${showScriptOutput ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log Level */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Log Level</p>
|
||||
<p className="text-sm text-gray-500">Agent log verbosity — Info shows the enrollment flow, Debug adds detail, Verbose adds every detail, Trace adds each internal step (every watcher, poll and queue event)</p>
|
||||
</div>
|
||||
<select
|
||||
value={logLevel}
|
||||
onChange={(e) => setLogLevel(e.target.value)}
|
||||
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
>
|
||||
<option value="Info">Info</option>
|
||||
<option value="Debug">Debug</option>
|
||||
<option value="Verbose">Verbose</option>
|
||||
<option value="Trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Enrollment Summary Dialog */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">Show Enrollment Summary</p>
|
||||
<p className="text-sm text-gray-500">Display a visual enrollment summary dialog to the end user after enrollment completes (success or failure). Requires the SummaryDialog companion to be deployed alongside the agent.</p>
|
||||
</div>
|
||||
<button onClick={() => setShowEnrollmentSummary(!showEnrollmentSummary)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${showEnrollmentSummary ? 'bg-violet-500' : 'bg-gray-300'}`}>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${showEnrollmentSummary ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showEnrollmentSummary && (
|
||||
<>
|
||||
{/* Auto-close timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Auto-Close Timeout</p>
|
||||
<p className="text-sm text-gray-500">Seconds until the summary dialog closes automatically. 0 = no auto-close.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={enrollmentSummaryTimeoutSeconds}
|
||||
onChange={(e) => setEnrollmentSummaryTimeoutSeconds(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Launch Retry Timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Launch Retry Timeout</p>
|
||||
<p className="text-sm text-gray-500">How long the agent retries launching the dialog when the desktop is locked by a credential UI (e.g. Windows Hello). 0 = no retry.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={3600}
|
||||
value={enrollmentSummaryLaunchRetrySeconds}
|
||||
onChange={(e) => setEnrollmentSummaryLaunchRetrySeconds(Math.min(3600, Math.max(0, parseInt(e.target.value) || 0)))}
|
||||
className="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 text-right focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 whitespace-nowrap">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branding Image URL */}
|
||||
<div className="p-4 rounded-lg border border-gray-200 hover:border-violet-200 transition-colors ml-4">
|
||||
<div className="mb-2">
|
||||
<p className="font-medium text-gray-900">Branding Image URL</p>
|
||||
<p className="text-sm text-gray-500">Optional banner image at the top of the summary dialog. Recommended size: 540 x 80 px. Larger images will be center-cropped.</p>
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
value={enrollmentSummaryBrandingImageUrl}
|
||||
onChange={(e) => setEnrollmentSummaryBrandingImageUrl(e.target.value)}
|
||||
placeholder="https://example.com/branding-banner.png"
|
||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-gray-900 focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ReadOnlyFieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Collectors */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<SectionCardHeader
|
||||
tone="emerald"
|
||||
iconPath="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
title="Agent Collectors"
|
||||
subtitle="Enable optional data collectors on enrolled devices. These generate additional telemetry traffic."
|
||||
docsPath={DOCS_PATHS.agentCollectors}
|
||||
/>
|
||||
<div className="p-6 space-y-5">
|
||||
<ReadOnlyFieldset readOnly={readOnly} notice={false}>
|
||||
<div className="space-y-5">
|
||||
{/* Performance Collector */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="font-medium text-gray-900">Performance Collector</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">CPU, memory, disk metrics at configurable intervals</p>
|
||||
{enablePerformanceCollector && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">Interval:</span>
|
||||
<input
|
||||
type="number"
|
||||
min={30}
|
||||
max={300}
|
||||
value={performanceCollectorInterval}
|
||||
onChange={(e) => setPerformanceCollectorInterval(parseInt(e.target.value) || 30)}
|
||||
onBlur={() => setPerformanceCollectorInterval(Math.max(30, Math.min(300, performanceCollectorInterval)))}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">seconds</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">Minimum: 30 seconds, Maximum: 300 seconds (5 minutes)</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnablePerformanceCollector(!enablePerformanceCollector)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enablePerformanceCollector ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enablePerformanceCollector ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* RealmJoin Watcher */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">RealmJoin Watcher</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Tracks RealmJoin deployment state during provisioning — deployment phase, per-package start and completion, and the RealmJoin completion gate. Off by default; enable only for tenants that provision devices with RealmJoin, elsewhere it produces no signal.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnableRealmJoinWatcher(!enableRealmJoinWatcher)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableRealmJoinWatcher ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableRealmJoinWatcher ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ConfigMgr Watcher */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900">ConfigMgr Watcher</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Shows in Session Info whether the Configuration Manager client is installed, with its service state, version and site code. Off by default; enable for tenants that co-manage devices with Configuration Manager.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEnableConfigMgrWatcher(!enableConfigMgrWatcher)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${enableConfigMgrWatcher ? 'bg-emerald-500' : 'bg-gray-300'}`}
|
||||
>
|
||||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enableConfigMgrWatcher ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Hello Wait Timeout */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-gray-200 hover:border-emerald-200 transition-colors">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="font-medium text-gray-900">Hello Wait Timeout</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">Seconds to wait for the Windows Hello wizard after ESP exit</p>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-600">Timeout:</span>
|
||||
<input
|
||||
type="number"
|
||||
min={HELLO_WAIT_TIMEOUT_MIN_SECONDS}
|
||||
max={HELLO_WAIT_TIMEOUT_MAX_SECONDS}
|
||||
value={helloWaitTimeoutSeconds}
|
||||
onChange={(e) => setHelloWaitTimeoutSeconds(parseInt(e.target.value) || HELLO_WAIT_TIMEOUT_DEFAULT_SECONDS)}
|
||||
onBlur={() => setHelloWaitTimeoutSeconds(clampHelloWaitTimeoutSeconds(helloWaitTimeoutSeconds))}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm text-gray-900 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-500">seconds</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">Minimum: 30 seconds, Maximum: 3600 seconds (1 hour). Values up to 300 seconds use the built-in 5-minute wait.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ReadOnlyFieldset>
|
||||
|
||||
{/* One Save/Reset bar for both cards: Agent Parameters and Agent Collectors are one
|
||||
section (agentSettings) and are saved together. */}
|
||||
{!readOnly && <SaveResetBar onSave={onSave} onReset={onReset} 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({
|
||||
|
||||
+23
-20
@@ -1,54 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
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. */
|
||||
saveBlockedReason?: string;
|
||||
}
|
||||
|
||||
export default function SaveResetBar({ onSave, onReset, saving, canSave = true }: 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);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
clearTimeout(savedTimer.current);
|
||||
setSaveResult("idle");
|
||||
try {
|
||||
await onSave();
|
||||
setSaveResult("saved");
|
||||
setTimeout(() => setSaveResult("idle"), 3000);
|
||||
} catch {
|
||||
savedTimer.current = setTimeout(() => setSaveResult("idle"), 3000);
|
||||
} catch (err) {
|
||||
// Stays until the next Save: unlike "Saved!" it needs reading and acting on.
|
||||
setFailureReason(apiErrorText(err, ""));
|
||||
setSaveResult("error");
|
||||
setTimeout(() => setSaveResult("idle"), 4000);
|
||||
}
|
||||
}, [onSave]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||
<div className="flex flex-wrap items-center justify-end gap-3 pt-4 border-t border-gray-200">
|
||||
{!canSave && (
|
||||
<span className="text-sm text-gray-400">Read-only</span>
|
||||
)}
|
||||
{saveBlockedReason && (
|
||||
<span className="min-w-0 break-words text-sm text-amber-600">{saveBlockedReason}</span>
|
||||
)}
|
||||
{/* Inline feedback (errors only — success is shown via button state) */}
|
||||
{saveResult === "error" && (
|
||||
<span className="flex items-center text-sm text-red-600 font-medium">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<span role="alert" className="flex items-center min-w-0 text-sm text-red-600 font-medium">
|
||||
<svg className="w-4 h-4 mr-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Save failed
|
||||
<span className="min-w-0 break-words">Save failed{failureReason && `: ${failureReason}`}</span>
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={onReset}
|
||||
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}
|
||||
disabled={saving || !canSave || !!saveBlockedReason}
|
||||
className={`px-4 py-2 rounded-md text-sm text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center space-x-2 ${
|
||||
saveResult === "saved" ? "bg-emerald-600" : "bg-green-600 hover:bg-green-700"
|
||||
}`}
|
||||
+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}
|
||||
/>
|
||||
|
||||
@@ -61,13 +61,8 @@ export const SECTION_FIELD_MAP = {
|
||||
contact: {
|
||||
fields: ["contactEmail", "companyName"],
|
||||
},
|
||||
agentSettings: {
|
||||
agentParameters: {
|
||||
fields: [
|
||||
"enablePerformanceCollector",
|
||||
"performanceCollectorIntervalSeconds",
|
||||
"helloWaitTimeoutSeconds",
|
||||
"enableRealmJoinWatcher",
|
||||
"enableConfigMgrWatcher",
|
||||
"selfDestructOnComplete",
|
||||
"keepLogFile",
|
||||
"rebootOnComplete",
|
||||
@@ -86,6 +81,15 @@ export const SECTION_FIELD_MAP = {
|
||||
"enrollmentSummaryLaunchRetrySeconds",
|
||||
],
|
||||
},
|
||||
agentCollectors: {
|
||||
fields: [
|
||||
"enablePerformanceCollector",
|
||||
"performanceCollectorIntervalSeconds",
|
||||
"helloWaitTimeoutSeconds",
|
||||
"enableRealmJoinWatcher",
|
||||
"enableConfigMgrWatcher",
|
||||
],
|
||||
},
|
||||
agentAnalyzers: {
|
||||
fields: [
|
||||
"enableLocalAdminAnalyzer",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -19,6 +19,11 @@ export function SectionContact() {
|
||||
const looksInvalid = trimmed.length > 0 && !trimmed.includes("@");
|
||||
// Mirrors TenantConfigValidation.MaxCompanyNameLength — the backend rejects longer values.
|
||||
const companyTooLong = companyName.trim().length > 200;
|
||||
const saveBlockedReason = looksInvalid
|
||||
? "Enter a valid email address to save."
|
||||
: companyTooLong
|
||||
? "Shorten the company name to save."
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
@@ -87,11 +92,10 @@ export function SectionContact() {
|
||||
</div>
|
||||
|
||||
{canEditConfig && (
|
||||
<SaveResetBar
|
||||
<SaveBar
|
||||
onSave={handleSaveContact}
|
||||
onReset={handleResetContact}
|
||||
saving={savingSection === "contact"}
|
||||
canSave={!looksInvalid && !companyTooLong}
|
||||
saveBlockedReason={saveBlockedReason}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+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