mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
feat(web): shared SectionCardHeader with docs links in every settings section
- SectionCardHeader replaces 33 hand-copied card headers across settings, rules and admin pages - DocsLink + lib/docsPaths: every tenant-facing section links its documentation page (new tab) - docsPaths guard test encodes the GitBook URL rule (group slug, not folder name) - delete dead MaintenanceSection duplicate Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { AdminConfiguration } from "@/types/adminConfig";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface AdminConfigSettingsSectionProps {
|
||||
loadingConfig: boolean;
|
||||
@@ -87,17 +88,12 @@ export function AdminConfigSettingsSection({
|
||||
}: 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">
|
||||
<div className="p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-blue-100 dark:from-indigo-900/40 dark:to-blue-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-indigo-900 dark:text-indigo-100">Global Settings</h2>
|
||||
<p className="text-sm text-indigo-600 dark:text-indigo-300 mt-1">Configure global settings for all tenants</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminIndigo"
|
||||
iconPath="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
title="Global Settings"
|
||||
subtitle="Configure global settings for all tenants"
|
||||
/>
|
||||
<div className="p-6">
|
||||
{loadingConfig ? (
|
||||
<div className="text-center py-8">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface ConfigReseedSectionProps {
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
@@ -148,17 +149,12 @@ export function ConfigReseedSection({
|
||||
<div className="space-y-6">
|
||||
{/* Fetch & Reseed from GitHub */}
|
||||
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-gray-800 dark:to-gray-800 border-2 border-emerald-300 dark:border-emerald-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-emerald-200 dark:border-emerald-700 bg-gradient-to-r from-emerald-100 to-teal-100 dark:from-emerald-900/40 dark:to-teal-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-emerald-900 dark:text-emerald-100">Fetch & Reseed from GitHub</h2>
|
||||
<p className="text-sm text-emerald-600 dark:text-emerald-300 mt-1">Fetch the latest rules from the GitHub repository and reseed all rule types</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminEmerald"
|
||||
iconPath="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
title="Fetch & Reseed from GitHub"
|
||||
subtitle="Fetch the latest rules from the GitHub repository and reseed all rule types"
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-sm text-emerald-900 dark:text-gray-200">
|
||||
This operation fetches all rule definitions from the GitHub repository and writes them to Azure Table Storage:
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TenantConfiguration } from "./TenantManagementSection";
|
||||
import { TenantSearchSelect } from "./TenantSearchSelect";
|
||||
import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform";
|
||||
import { firstBlockedSessionId, blockedSessionCount } from "./blockedDeviceHelpers";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface ResolvedDevice {
|
||||
sessionId: string;
|
||||
@@ -309,17 +310,12 @@ function DeviceBlockSectionInner({
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-red-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border-2 border-red-300 dark:border-red-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-red-200 dark:border-red-700 bg-gradient-to-r from-red-100 to-orange-100 dark:from-red-900/40 dark:to-orange-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-red-900 dark:text-red-100">Device Block Management</h2>
|
||||
<p className="text-sm text-red-600 dark:text-red-300 mt-1">Temporarily block a rogue device from sending data. The agent will stop uploading when it receives the block signal.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminRed"
|
||||
iconPath="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
title="Device Block Management"
|
||||
subtitle="Temporarily block a rogue device from sending data. The agent will stop uploading when it receives the block signal."
|
||||
/>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Block / Kill a device form */}
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ValidationIndicator } from "@/components/ValidationIndicator";
|
||||
import { BuiltInSectionsList } from "@/components/diagnostics/BuiltInSectionsList";
|
||||
import { DiagnosticsPathRow } from "@/components/diagnostics/DiagnosticsPathRow";
|
||||
import type { DiagnosticsBuiltInSection, DiagnosticsLogPath } from "@/types/diagnostics";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
// Re-export so existing consumers importing from this component keep working
|
||||
export type { DiagnosticsLogPath };
|
||||
@@ -52,19 +53,12 @@ export function DiagnosticsLogPathsSection({
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-teal-50 to-cyan-50 dark:from-gray-800 dark:to-gray-800 border-2 border-teal-300 dark:border-teal-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-teal-200 dark:border-teal-700 bg-gradient-to-r from-teal-100 to-cyan-100 dark:from-teal-900/40 dark:to-cyan-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-teal-600 dark:text-teal-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-teal-900 dark:text-teal-100">Diagnostics Log Paths</h2>
|
||||
<p className="text-sm text-teal-600 dark:text-teal-300 mt-1">
|
||||
What every diagnostics package collects: the built-in sections compiled into the agent, plus the global paths below sent to all tenants. Tenants may add their own paths in Settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminTeal"
|
||||
iconPath="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
title="Diagnostics Log Paths"
|
||||
subtitle="What every diagnostics package collects: the built-in sections compiled into the agent, plus the global paths below sent to all tenants. Tenants may add their own paths in Settings."
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Info box */}
|
||||
<div className="bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-700 rounded-lg p-3 flex items-start space-x-2">
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
|
||||
interface MaintenanceSectionProps {
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
setError: (error: string | null) => void;
|
||||
setSuccessMessage: (message: string | null) => void;
|
||||
}
|
||||
|
||||
export function MaintenanceSection({
|
||||
getAccessToken,
|
||||
setError,
|
||||
setSuccessMessage,
|
||||
}: MaintenanceSectionProps) {
|
||||
const [triggeringMaintenance, setTriggeringMaintenance] = useState(false);
|
||||
const [maintenanceDate, setMaintenanceDate] = useState<string>("");
|
||||
// Latest selectable day (yesterday), fixed at mount — render must stay pure
|
||||
// (react-hooks/purity) and the cap doesn't need a live midnight rollover.
|
||||
const [maxMaintenanceDate] = useState(() => new Date(Date.now() - 86400000).toISOString().split('T')[0]);
|
||||
const [reseedingRules, setReseedingRules] = useState(false);
|
||||
const [reseedingGatherRules, setReseedingGatherRules] = useState(false);
|
||||
const [reseedingImePatterns, setReseedingImePatterns] = useState(false);
|
||||
const [fetchingFromGitHub, setFetchingFromGitHub] = useState(false);
|
||||
|
||||
const handleTriggerMaintenance = async () => {
|
||||
try {
|
||||
setTriggeringMaintenance(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const response = await authenticatedFetch(api.maintenance.trigger(maintenanceDate || undefined), getAccessToken, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to trigger maintenance: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const dateInfo = maintenanceDate ? ` for ${maintenanceDate}` : '';
|
||||
setSuccessMessage(`Maintenance job completed successfully${dateInfo}!`);
|
||||
|
||||
// Auto-hide success message after 5 seconds
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenExpiredError) {
|
||||
console.error("Session expired while triggering maintenance");
|
||||
} else {
|
||||
console.error("Error triggering maintenance:", err);
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Failed to trigger maintenance job");
|
||||
} finally {
|
||||
setTriggeringMaintenance(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReseedAnalyzeRules = async () => {
|
||||
try {
|
||||
setReseedingRules(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const response = await authenticatedFetch(api.rules.reseedFromGitHub("analyze"), getAccessToken, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to reseed analyze rules: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setSuccessMessage(`Analyze rules reseeded from GitHub: ${result.analyze?.deleted ?? 0} deleted, ${result.analyze?.written ?? 0} written`);
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenExpiredError) {
|
||||
console.error("Session expired while reseeding analyze rules");
|
||||
} else {
|
||||
console.error("Error reseeding analyze rules:", err);
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Failed to reseed analyze rules");
|
||||
} finally {
|
||||
setReseedingRules(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReseedGatherRules = async () => {
|
||||
try {
|
||||
setReseedingGatherRules(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const response = await authenticatedFetch(api.rules.reseedFromGitHub("gather"), getAccessToken, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to reseed gather rules: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setSuccessMessage(`Gather rules reseeded from GitHub: ${result.gather?.deleted ?? 0} deleted, ${result.gather?.written ?? 0} written`);
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenExpiredError) {
|
||||
console.error("Session expired while reseeding gather rules");
|
||||
} else {
|
||||
console.error("Error reseeding gather rules:", err);
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Failed to reseed gather rules");
|
||||
} finally {
|
||||
setReseedingGatherRules(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReseedImePatterns = async () => {
|
||||
try {
|
||||
setReseedingImePatterns(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const response = await authenticatedFetch(api.rules.reseedFromGitHub("ime"), getAccessToken, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to reseed IME log patterns: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setSuccessMessage(`IME log patterns reseeded from GitHub: ${result.ime?.deleted ?? 0} deleted, ${result.ime?.written ?? 0} written`);
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenExpiredError) {
|
||||
console.error("Session expired while reseeding IME log patterns");
|
||||
} else {
|
||||
console.error("Error reseeding IME log patterns:", err);
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Failed to reseed IME log patterns");
|
||||
} finally {
|
||||
setReseedingImePatterns(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchFromGitHub = async () => {
|
||||
try {
|
||||
setFetchingFromGitHub(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
const response = await authenticatedFetch(api.rules.reseedFromGitHub("all"), getAccessToken, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to reseed from GitHub: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setSuccessMessage(
|
||||
`GitHub Reseed complete: Gather (${result.gather?.deleted ?? 0} deleted, ${result.gather?.written ?? 0} written), ` +
|
||||
`Analyze (${result.analyze?.deleted ?? 0} deleted, ${result.analyze?.written ?? 0} written), ` +
|
||||
`IME (${result.ime?.deleted ?? 0} deleted, ${result.ime?.written ?? 0} written)`
|
||||
);
|
||||
setTimeout(() => setSuccessMessage(null), 8000);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenExpiredError) {
|
||||
console.error("Session expired while reseeding from GitHub");
|
||||
} else {
|
||||
console.error("Error reseeding from GitHub:", err);
|
||||
}
|
||||
setError(err instanceof Error ? err.message : "Failed to reseed from GitHub");
|
||||
} finally {
|
||||
setFetchingFromGitHub(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Manual Maintenance Trigger */}
|
||||
<div className="bg-gradient-to-br from-purple-50 to-violet-50 dark:from-gray-800 dark:to-gray-800 border-2 border-purple-300 dark:border-purple-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-purple-200 dark:border-purple-700 bg-gradient-to-r from-purple-100 to-violet-100 dark:from-purple-900/40 dark:to-violet-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-purple-900 dark:text-purple-100">Manual Maintenance Trigger</h2>
|
||||
<p className="text-sm text-purple-600 dark:text-purple-300 mt-1">Execute platform-wide maintenance operations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-sm text-purple-900 dark:text-gray-200">
|
||||
Runs all timer-triggered tasks plus additional backfill & repair operations that only run on manual trigger.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-purple-700 dark:text-purple-300 uppercase tracking-wide mb-1">Standard tasks (also run by 2h timer)</p>
|
||||
<ul className="text-sm text-purple-900 dark:text-gray-200 space-y-1 ml-4">
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-500 dark:text-purple-400 mr-2">•</span>
|
||||
<span>Mark stalled sessions as timed out</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-500 dark:text-purple-400 mr-2">•</span>
|
||||
<span>Aggregate metrics into historical snapshots</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-500 dark:text-purple-400 mr-2">•</span>
|
||||
<span>Clean up old data based on retention policies</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-purple-500 dark:text-purple-400 mr-2">•</span>
|
||||
<span>Recompute platform-wide statistics</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-amber-700 dark:text-amber-300 uppercase tracking-wide mb-1">Manual-only tasks (backfill & repair)</p>
|
||||
<ul className="text-sm text-purple-900 dark:text-gray-200 space-y-1 ml-4">
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-500 dark:text-amber-400 mr-2">•</span>
|
||||
<span>Backfill sessions missing from SessionsIndex</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-500 dark:text-amber-400 mr-2">•</span>
|
||||
<span>Clean up ghost SessionsIndex entries <span className="text-xs text-gray-500 dark:text-gray-400">(remove after 2026-06)</span></span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-amber-500 dark:text-amber-400 mr-2">•</span>
|
||||
<span>Backfill tenant OnboardedAt dates <span className="text-xs text-gray-500 dark:text-gray-400">(remove when complete)</span></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 dark:bg-gray-700 border border-purple-200 dark:border-purple-600 rounded-lg p-4">
|
||||
<label className="block text-sm font-medium text-purple-800 dark:text-purple-200 mb-1">
|
||||
Target Date (optional)
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={maintenanceDate}
|
||||
onChange={(e) => setMaintenanceDate(e.target.value)}
|
||||
max={maxMaintenanceDate}
|
||||
className="w-full max-w-xs px-3 py-2 border border-purple-300 dark:border-purple-600 rounded-lg text-sm bg-white dark:bg-gray-600 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
Leave empty to run the standard maintenance with automatic catch-up (aggregates any missed days within the last 7 days).
|
||||
Select a specific date to manually aggregate metrics for that day, e.g. to backfill data older than 7 days.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-700 border border-purple-300 dark:border-purple-600 rounded-lg p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<svg className="w-5 h-5 text-purple-600 dark:text-purple-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">
|
||||
<strong>Warning:</strong> This operation runs across all tenants and may take several minutes to complete.
|
||||
Use this only for testing or when immediate cleanup is needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
onClick={handleTriggerMaintenance}
|
||||
disabled={triggeringMaintenance}
|
||||
className="px-6 py-3 bg-gradient-to-r from-purple-600 to-violet-600 text-white rounded-lg hover:from-purple-700 hover:to-violet-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-md hover:shadow-lg flex items-center space-x-2"
|
||||
>
|
||||
{triggeringMaintenance ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
<span>Running...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Run Now</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fetch & Reseed from GitHub */}
|
||||
<div id="fetch-and-reseed" className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-gray-800 dark:to-gray-800 border-2 border-emerald-300 dark:border-emerald-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-emerald-200 dark:border-emerald-700 bg-gradient-to-r from-emerald-100 to-teal-100 dark:from-emerald-900/40 dark:to-teal-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-emerald-900 dark:text-emerald-100">Fetch & Reseed from GitHub</h2>
|
||||
<p className="text-sm text-emerald-600 dark:text-emerald-300 mt-1">Fetch the latest rules from the GitHub repository and reseed all rule types</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-sm text-emerald-900 dark:text-gray-200">
|
||||
This operation fetches all rule definitions from the GitHub repository and writes them to Azure Table Storage:
|
||||
</p>
|
||||
<ul className="text-sm text-emerald-900 dark:text-gray-200 space-y-1 ml-4">
|
||||
<li className="flex items-start">
|
||||
<span className="text-emerald-500 dark:text-emerald-400 mr-2">•</span>
|
||||
<span>Fetches Gather Rules, Analyze Rules, and IME Log Patterns from GitHub</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-emerald-500 dark:text-emerald-400 mr-2">•</span>
|
||||
<span>Deletes all existing global built-in entries and writes the latest versions</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<span className="text-emerald-500 dark:text-emerald-400 mr-2">•</span>
|
||||
<span>Tenant-specific custom rules and overrides are not affected</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<svg className="w-5 h-5 text-blue-500 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
Rules are loaded from <code className="px-1 bg-blue-100 dark:bg-blue-800 rounded text-xs">raw.githubusercontent.com</code>. After a merge to the repository, it may take up to <strong>5 minutes</strong>{" "}for GitHub's CDN cache to update. If you see stale rules after reseeding, wait a few minutes and try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end pt-2">
|
||||
<button
|
||||
onClick={handleFetchFromGitHub}
|
||||
disabled={fetchingFromGitHub}
|
||||
className="px-6 py-3 bg-gradient-to-r from-emerald-600 to-teal-600 text-white rounded-lg hover:from-emerald-700 hover:to-teal-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-md hover:shadow-lg flex items-center space-x-2"
|
||||
>
|
||||
{fetchingFromGitHub ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
<span>Fetching from GitHub...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Fetch & Reseed All</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Individual Reseed Sections */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Reseed Analyze Rules */}
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border border-amber-200 dark:border-amber-700 rounded-lg shadow">
|
||||
<div className="p-4 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40">
|
||||
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">Reseed Analyze Rules</h3>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-300 mt-1">Fetch and reseed only analyze rules from GitHub</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleReseedAnalyzeRules}
|
||||
disabled={reseedingRules}
|
||||
className="px-4 py-2 bg-gradient-to-r from-amber-600 to-orange-600 text-white rounded-lg hover:from-amber-700 hover:to-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow flex items-center space-x-2 text-sm"
|
||||
>
|
||||
{reseedingRules ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Reseeding...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Reseed Analyze</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reseed Gather Rules */}
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border border-amber-200 dark:border-amber-700 rounded-lg shadow">
|
||||
<div className="p-4 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40">
|
||||
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">Reseed Gather Rules</h3>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-300 mt-1">Fetch and reseed only gather rules from GitHub</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleReseedGatherRules}
|
||||
disabled={reseedingGatherRules}
|
||||
className="px-4 py-2 bg-gradient-to-r from-amber-600 to-orange-600 text-white rounded-lg hover:from-amber-700 hover:to-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow flex items-center space-x-2 text-sm"
|
||||
>
|
||||
{reseedingGatherRules ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Reseeding...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Reseed Gather</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reseed IME Log Patterns */}
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border border-amber-200 dark:border-amber-700 rounded-lg shadow">
|
||||
<div className="p-4 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40">
|
||||
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">Reseed IME Log Patterns</h3>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-300 mt-1">Fetch and reseed only IME log patterns from GitHub</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleReseedImePatterns}
|
||||
disabled={reseedingImePatterns}
|
||||
className="px-4 py-2 bg-gradient-to-r from-amber-600 to-orange-600 text-white rounded-lg hover:from-amber-700 hover:to-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow flex items-center space-x-2 text-sm"
|
||||
>
|
||||
{reseedingImePatterns ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Reseeding...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<span>Reseed IME</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface MaintenanceTriggerSectionProps {
|
||||
getAccessToken: () => Promise<string | null>;
|
||||
@@ -87,17 +88,12 @@ export function MaintenanceTriggerSection({
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-purple-50 to-violet-50 dark:from-gray-800 dark:to-gray-800 border-2 border-purple-300 dark:border-purple-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-purple-200 dark:border-purple-700 bg-gradient-to-r from-purple-100 to-violet-100 dark:from-purple-900/40 dark:to-violet-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-purple-900 dark:text-purple-100">Manual Maintenance Trigger</h2>
|
||||
<p className="text-sm text-purple-600 dark:text-purple-300 mt-1">Execute platform-wide maintenance operations</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminPurple"
|
||||
iconPath="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
title="Manual Maintenance Trigger"
|
||||
subtitle="Execute platform-wide maintenance operations"
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-sm text-purple-900 dark:text-gray-200">
|
||||
Runs all timer-triggered tasks plus additional backfill & repair operations that only run on manual trigger.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChannelEditor } from "@/components/notifications/ChannelEditor";
|
||||
import { MAX_NOTIFICATION_CHANNELS, type NotificationChannel } from "@/app/settings/types";
|
||||
import { toggleChannelBinding } from "./opsChannelRouting";
|
||||
import { AUTO_ACTION_MODES, describeAutoActionWarning, type AutoActionMode } from "./excessiveEventAutoAction";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
// All known ops event types grouped by category
|
||||
const OPS_EVENT_TYPES: Record<string, string[]> = {
|
||||
@@ -351,19 +352,12 @@ export function OpsAlertRulesSection({
|
||||
<div className="space-y-6">
|
||||
{/* Alert Rules */}
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border-2 border-amber-300 dark:border-amber-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-amber-900 dark:text-amber-100">Alert Rules</h2>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-300 mt-1">
|
||||
Select which ops events trigger alerts. {enabledRulesCount} rule(s) active, {enabledProviders} provider(s) enabled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminAmber"
|
||||
iconPath="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
title="Alert Rules"
|
||||
subtitle={`Select which ops events trigger alerts. ${enabledRulesCount} rule(s) active, ${enabledProviders} provider(s) enabled.`}
|
||||
/>
|
||||
<div className="p-6">
|
||||
{loadingConfig ? (
|
||||
<div className="text-center py-8">
|
||||
@@ -526,19 +520,12 @@ export function OpsAlertRulesSection({
|
||||
|
||||
{/* Channels */}
|
||||
<div className="bg-gradient-to-br from-sky-50 to-cyan-50 dark:from-gray-800 dark:to-gray-800 border-2 border-sky-300 dark:border-sky-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-sky-200 dark:border-sky-700 bg-gradient-to-r from-sky-100 to-cyan-100 dark:from-sky-900/40 dark:to-cyan-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-sky-600 dark:text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-sky-900 dark:text-sky-100">Alert Channels</h2>
|
||||
<p className="text-sm text-sky-600 dark:text-sky-300 mt-1">
|
||||
Named destinations for platform alerts. Each rule above chooses which of these it notifies.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminSky"
|
||||
iconPath="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
title="Alert Channels"
|
||||
subtitle="Named destinations for platform alerts. Each rule above chooses which of these it notifies."
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
{channels.length === 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -8,6 +8,7 @@ import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { extractContinuation } from "@/lib/paginationLink";
|
||||
import { extractSessionId, buildAutoReason } from "./opsEventSessionHelpers";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface OpsEvent {
|
||||
id: string;
|
||||
@@ -242,20 +243,13 @@ export function OpsEventsSection({
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-slate-50 to-gray-50 dark:from-gray-800 dark:to-gray-800 border-2 border-slate-300 dark:border-slate-700 rounded-lg shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-100 to-gray-100 dark:from-slate-900/40 dark:to-gray-900/40">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900 dark:text-slate-100">Operational Events</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
||||
Vital infrastructure events across consent, maintenance, security, and tenant lifecycle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionCardHeader
|
||||
tone="adminSlate"
|
||||
iconPath="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
title="Operational Events"
|
||||
subtitle="Vital infrastructure events across consent, maintenance, security, and tenant lifecycle."
|
||||
trailing={
|
||||
<>
|
||||
<Link
|
||||
href="/admin/settings/alerts"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
@@ -272,9 +266,9 @@ export function OpsEventsSection({
|
||||
>
|
||||
{loading ? "Loading..." : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{!loading && filteredEvents.length > 0 && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TenantConfiguration } from "./TenantManagementSection";
|
||||
import { TenantSearchSelect } from "./TenantSearchSelect";
|
||||
import { SessionExportEvent, generateCsvExport, generateUiExport } from "@/utils/sessionExportUtils";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
function downloadFile(content: string, filename: string, mimeType: string) {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
@@ -66,17 +67,12 @@ export function SessionExportSection({
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-teal-50 to-cyan-50 dark:from-gray-800 dark:to-gray-800 border-2 border-teal-300 dark:border-teal-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-teal-200 dark:border-teal-700 bg-gradient-to-r from-teal-100 to-cyan-100 dark:from-teal-900/40 dark:to-cyan-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-teal-600 dark:text-teal-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-teal-900 dark:text-teal-100">Session Event Export</h2>
|
||||
<p className="text-sm text-teal-600 dark:text-teal-300 mt-1">Fetch and export all events directly from storage — use to analyze timeline phase grouping and ordering</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminTeal"
|
||||
iconPath="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
title="Session Event Export"
|
||||
subtitle="Fetch and export all events directly from storage — use to analyze timeline phase grouping and ordering"
|
||||
/>
|
||||
<div className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isGuid } from "@/utils/inputValidation";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform";
|
||||
import { TableSkeleton } from "@/components/skeletons/TableSkeleton";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -367,21 +368,12 @@ function SessionReportsSectionInner({
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-gray-800 dark:to-gray-800 border-2 border-indigo-300 dark:border-indigo-700 rounded-lg shadow-lg">
|
||||
{/* Section Header */}
|
||||
<div className="p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-purple-100 dark:from-indigo-900/40 dark:to-purple-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-semibold text-indigo-900 dark:text-indigo-100">Session Reports</h2>
|
||||
</div>
|
||||
<p className="text-sm text-indigo-600 dark:text-indigo-300 mt-1">
|
||||
Sessions reported by Tenant Admins for analysis
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminIndigoPurple"
|
||||
iconPath="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
|
||||
title="Session Reports"
|
||||
subtitle="Sessions reported by Tenant Admins for analysis"
|
||||
/>
|
||||
|
||||
{/* Tenant filter + refresh */}
|
||||
<div className="px-6 pt-4 pb-2 flex items-center gap-2 flex-wrap">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface BlockedVersion {
|
||||
versionPattern: string;
|
||||
@@ -126,17 +127,12 @@ export function VersionBlockSection({
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-gray-800 dark:to-gray-800 border-2 border-amber-300 dark:border-amber-700 rounded-lg shadow-lg">
|
||||
<div className="p-6 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-amber-900 dark:text-amber-100">Version Block Management</h2>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-300 mt-1">Block or kill agents by version pattern. Useful for catching old agents that are still running.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminAmber"
|
||||
iconPath="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
title="Version Block Management"
|
||||
subtitle="Block or kill agents by version pattern. Useful for catching old agents that are still running."
|
||||
/>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Pattern help */}
|
||||
<div className="border border-amber-200 dark:border-amber-700 rounded-lg p-4 bg-amber-50/50 dark:bg-amber-900/10">
|
||||
|
||||
@@ -7,6 +7,7 @@ import SaveResetBar from "../../settings/components/SaveResetBar";
|
||||
import VulnerabilityExposurePanel from "@/components/VulnerabilityExposurePanel";
|
||||
import type { AdminConfiguration } from "@/types/adminConfig";
|
||||
import { useCanMutatePlatform } from "@/hooks/useCanMutatePlatform";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
interface VulnerabilityTopCve {
|
||||
cveId: string;
|
||||
@@ -422,17 +423,12 @@ export function VulnerabilityDataSection({
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-gray-800 dark:to-gray-800 border-2 border-emerald-300 dark:border-emerald-700 rounded-lg shadow-lg">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-emerald-200 dark:border-emerald-700 bg-gradient-to-r from-emerald-100 to-teal-100 dark:from-emerald-900/40 dark:to-teal-900/40">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-emerald-600 dark:text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-emerald-900 dark:text-emerald-100">Vulnerability Data Configuration</h2>
|
||||
<p className="text-sm text-emerald-600 dark:text-emerald-300 mt-1">Configure vulnerability correlation data sources and API keys.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="adminEmerald"
|
||||
iconPath="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"
|
||||
title="Vulnerability Data Configuration"
|
||||
subtitle="Configure vulnerability correlation data sources and API keys."
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
|
||||
+7
-13
@@ -7,6 +7,7 @@ import { useTenantList } from "@/hooks/useTenantList";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { api } from "@/lib/api";
|
||||
import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
/** One delegated-admin assignment as returned by /api/global/delegated-admins (camelCase JSON). */
|
||||
interface DelegatedAssignment {
|
||||
@@ -191,19 +192,12 @@ export function SectionDelegatedAdmins() {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-indigo-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 21a9 9 0 100-18 9 9 0 000 18zm0 0c2.5 0 4.5-4 4.5-9S14.5 3 12 3 7.5 7 7.5 12s2 9 4.5 9zM3 12h18" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Delegated Admins (MSP Mode)</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Grant a person read access to a subset of tenants (a “fleet”), without making them a
|
||||
full platform admin. They sign in to their own tenant and see only the tenants you grant here.
|
||||
</p>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="skyIndigo"
|
||||
iconPath="M12 21a9 9 0 100-18 9 9 0 000 18zm0 0c2.5 0 4.5-4 4.5-9S14.5 3 12 3 7.5 7 7.5 12s2 9 4.5 9zM3 12h18"
|
||||
title="Delegated Admins (MSP Mode)"
|
||||
subtitle="Grant a person read access to a subset of tenants (a “fleet”), without making them a full platform admin. They sign in to their own tenant and see only the tenants you grant here."
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Notifications */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTenantList } from "@/hooks/useTenantList";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { api } from "@/lib/api";
|
||||
import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
|
||||
/** One UPN assigned to a group (camelCase JSON from the backend). */
|
||||
interface GroupAssignee {
|
||||
@@ -245,19 +246,12 @@ export function SectionTenantGroups() {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-indigo-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Tenant Groups (MSP Mode)</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Bundle tenants into a named group, then assign people to the group instead of to each tenant.
|
||||
Add a tenant once and every assignee gets it; manage your managed-service team by who is assigned here.
|
||||
</p>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="skyIndigo"
|
||||
iconPath="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
title="Tenant Groups (MSP Mode)"
|
||||
subtitle="Bundle tenants into a named group, then assign people to the group instead of to each tenant. Add a tenant once and every assignee gets it; manage your managed-service team by who is assigned here."
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Notifications */}
|
||||
|
||||
@@ -31,7 +31,8 @@ function validateEvaluateOnForm(form: RuleForm): string | null {
|
||||
import AnalyzeRuleFormFields from "./components/AnalyzeRuleFormFields";
|
||||
import AnalyzeRuleCard, { RuleTrendPoint, RuleRegressionInfo } from "./components/AnalyzeRuleCard";
|
||||
import TemplateConfigModal from "./components/TemplateConfigModal";
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
export default function AnalyzeRulesPage() {
|
||||
const { user, getAccessToken } = useAuth();
|
||||
@@ -768,16 +769,13 @@ export default function AnalyzeRulesPage() {
|
||||
{/* Create Custom Rule Form */}
|
||||
{showCreateForm && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Create Custom Analyze Rule</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Define conditions and confidence scoring for issue detection — <a href={`${DOCS_URL}/rules/analyze-rules`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:text-green-800 underline">see documentation</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{/* JSON Mode Toggle */}
|
||||
<SectionCardHeader
|
||||
tone="indigoPurple"
|
||||
iconPath="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
title="Create Custom Analyze Rule"
|
||||
subtitle="Define conditions and confidence scoring for issue detection"
|
||||
docsPath={DOCS_PATHS.analyzeRules}
|
||||
trailing={
|
||||
<JsonModeToggleButtons
|
||||
jsonMode={jsonModeCreate}
|
||||
onToggleMode={(mode) => {
|
||||
@@ -786,8 +784,8 @@ export default function AnalyzeRulesPage() {
|
||||
setJsonError(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="p-6">
|
||||
<FormJsonToggle
|
||||
jsonMode={jsonModeCreate}
|
||||
|
||||
@@ -17,7 +17,8 @@ import { TenantScopeSelector } from "@/components/TenantScopeSelector";
|
||||
import { GatherRule, NewRuleForm, PastedGatherJson, EMPTY_FORM, PHASE_TRIGGERS, buildScopeFields, gatherRuleToForm, targetBlocked, validateScopeSelection } from "./types";
|
||||
import { GatherRuleFormFields } from "./components/GatherRuleFormFields";
|
||||
import { GatherRuleCard } from "./components/GatherRuleCard";
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
export default function GatherRulesPage() {
|
||||
const { user, getAccessToken } = useAuth();
|
||||
@@ -484,17 +485,13 @@ export default function GatherRulesPage() {
|
||||
{/* Create Custom Rule Form */}
|
||||
{showCreateForm && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Create Custom Rule</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Define a new data collection rule for enrolled devices — <a href={`${DOCS_URL}/rules/gather-rules`} target="_blank" rel="noopener noreferrer" className="text-green-700 hover:text-green-800 underline">see documentation</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="indigoPurple"
|
||||
iconPath="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
title="Create Custom Rule"
|
||||
subtitle="Define a new data collection rule for enrolled devices"
|
||||
docsPath={DOCS_PATHS.gatherRules}
|
||||
trailing={
|
||||
<JsonModeToggleButtons
|
||||
jsonMode={jsonModeCreate}
|
||||
onToggleMode={(mode) => {
|
||||
@@ -503,8 +500,8 @@ export default function GatherRulesPage() {
|
||||
setJsonError(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="p-6">
|
||||
<FormJsonToggle
|
||||
jsonMode={jsonModeCreate}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { TenantAdmin } from "../types";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface AdminManagementSectionProps {
|
||||
admins: TenantAdmin[];
|
||||
@@ -75,17 +77,13 @@ export default function AdminManagementSection({
|
||||
}: AdminManagementSectionProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-indigo-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Access Management</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Manage team members and their roles for this tenant</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="purple"
|
||||
iconPath="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
title="Access Management"
|
||||
subtitle="Manage team members and their roles for this tenant"
|
||||
docsPath={DOCS_PATHS.accessManagement}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface AgentAnalyzersSectionProps {
|
||||
enableLocalAdminAnalyzer: boolean;
|
||||
@@ -73,17 +75,13 @@ export default function AgentAnalyzersSection({
|
||||
|
||||
return (
|
||||
<div id="agent-analyzers" className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-rose-50 to-red-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Agent Analyzers</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Security analyzers that run on enrolled devices to detect configuration anomalies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="rose"
|
||||
iconPath="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
title="Agent Analyzers"
|
||||
subtitle="Security analyzers that run on enrolled devices to detect configuration anomalies."
|
||||
docsPath={DOCS_PATHS.agentAnalyzers}
|
||||
/>
|
||||
<div className="p-6 space-y-6">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface AgentSettingsSectionProps {
|
||||
enablePerformanceCollector: boolean;
|
||||
@@ -101,17 +103,13 @@ export default function AgentSettingsSection({
|
||||
<>
|
||||
{/* Agent Parameters */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-violet-50 to-purple-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Agent Parameters</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Control agent behavior on enrolled devices. Changes take effect on the next agent config refresh.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -361,17 +359,13 @@ export default function AgentSettingsSection({
|
||||
|
||||
{/* Agent Collectors */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-emerald-50 to-teal-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Agent Collectors</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Enable optional data collectors on enrolled devices. These generate additional telemetry traffic.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
+11
-17
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface AutopilotValidationSectionProps {
|
||||
validateAutopilotDevice: boolean;
|
||||
@@ -120,26 +122,18 @@ export default function AutopilotValidationSection({
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Enrollment Device Validation
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Validate devices against Intune registrations before accepting agent data (mandatory for agent ingestion)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="amber"
|
||||
iconPath="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
title="Enrollment Device Validation"
|
||||
subtitle="Validate devices against Intune registrations before accepting agent data (mandatory for agent ingestion)"
|
||||
docsPath={DOCS_PATHS.enrollmentDeviceValidation}
|
||||
trailing={
|
||||
<span className={`flex-shrink-0 inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${anyValidationEnabled ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`}>
|
||||
{anyValidationEnabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="p-6 space-y-5">
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 space-y-2">
|
||||
<p className="text-sm text-gray-700">
|
||||
|
||||
+11
-30
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { BOOTSTRAP_GO_URL } from "@/utils/config";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
// Display-only: the authoritative URL comes from the backend's bootstrapUrl
|
||||
// (Constants.BootstrapGoBaseUrl); this host renders the sessions list rows.
|
||||
@@ -131,34 +133,13 @@ export default function BootstrapSessionsSection({
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-cyan-50 to-teal-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-cyan-100 rounded-lg">
|
||||
<svg
|
||||
className="w-6 h-6 text-cyan-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
OOBE Bootstrap
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
Generate time-limited URLs to install the agent before Intune
|
||||
enrollment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="cyan"
|
||||
iconPath="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
title="OOBE Bootstrap"
|
||||
subtitle="Generate time-limited URLs to install the agent before Intune enrollment"
|
||||
docsPath={DOCS_PATHS.bootstrapSessions}
|
||||
trailing={
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
@@ -179,8 +160,8 @@ export default function BootstrapSessionsSection({
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Info banner */}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface DataManagementSectionProps {
|
||||
dataRetentionDays: number;
|
||||
@@ -35,17 +37,13 @@ export default function DataManagementSection({
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-green-50 to-emerald-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Data Management</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Configure data retention and session timeout policies</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="green"
|
||||
iconPath="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
||||
title="Data Management"
|
||||
subtitle="Configure data retention and session timeout policies"
|
||||
docsPath={DOCS_PATHS.dataManagement}
|
||||
/>
|
||||
<div className="p-6 space-y-6">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -10,6 +10,8 @@ import { ContextPill, DiagnosticsPathRow } from "@/components/diagnostics/Diagno
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { parseSasExpiry } from "./diagnosticsSasExpiry";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
// Re-export for callers that still import from the section. Implementation lives
|
||||
// in `diagnosticsSasExpiry.ts` so vitest can test it without pulling in JSX.
|
||||
@@ -89,17 +91,13 @@ export default function DiagnosticsSection({
|
||||
|
||||
return (
|
||||
<div id="diagnostics" className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Diagnostics Package</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Upload diagnostic files as a ZIP package to your Azure Blob Storage after enrollment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="amber"
|
||||
iconPath="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
title="Diagnostics Package"
|
||||
subtitle="Upload diagnostic files as a ZIP package to your Azure Blob Storage after enrollment."
|
||||
docsPath={DOCS_PATHS.diagnosticsPackage}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState } from "react";
|
||||
import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { joinList, parseList } from "../lib/hardwareWhitelist";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface HardwareWhitelistSectionProps {
|
||||
manufacturerWhitelist: string;
|
||||
@@ -136,22 +138,13 @@ export default function HardwareWhitelistSection({
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-blue-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
Hardware Whitelist
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Control which device manufacturers and models the backend accepts
|
||||
monitoring data from
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="indigo"
|
||||
iconPath="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||
title="Hardware Whitelist"
|
||||
subtitle="Control which device manufacturers and models the backend accepts monitoring data from"
|
||||
docsPath={DOCS_PATHS.hardwareWhitelist}
|
||||
/>
|
||||
<div className="p-6 space-y-5">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-5">
|
||||
|
||||
@@ -7,6 +7,8 @@ import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch"
|
||||
import { api } from "@/lib/api";
|
||||
import { useTenantList } from "@/hooks/useTenantList";
|
||||
import { HOME_TENANT_UNRESOLVED } from "@/lib/identityBinding";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface McpUser {
|
||||
upn: string;
|
||||
@@ -286,18 +288,13 @@ export default function McpUsersSection() {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-indigo-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-gray-900">MCP User Management</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Control who can access the AI agent interface (Model Context Protocol) and assign individual usage plans.
|
||||
</p>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="purple"
|
||||
iconPath="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
title="MCP User Management"
|
||||
subtitle="Control who can access the AI agent interface (Model Context Protocol) and assign individual usage plans."
|
||||
docsPath={DOCS_PATHS.mcpUsers}
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Notifications */}
|
||||
|
||||
@@ -4,6 +4,8 @@ import SaveResetBar from "./SaveResetBar";
|
||||
import ReadOnlyFieldset from "./ReadOnlyFieldset";
|
||||
import { MAX_NOTIFICATION_CHANNELS, NotificationChannel } from "../types";
|
||||
import { ChannelEditor } from "@/components/notifications/ChannelEditor";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface NotificationsSectionProps {
|
||||
channels: NotificationChannel[];
|
||||
@@ -51,19 +53,13 @@ export default function NotificationsSection({
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-blue-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-sky-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Notifications</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Send notifications to one or more channels — Teams, Slack, Discord, or any JSON webhook (ticketing, automation, bots). Each channel picks which events it receives.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="sky"
|
||||
iconPath="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
title="Notifications"
|
||||
subtitle="Send notifications to one or more channels — Teams, Slack, Discord, or any JSON webhook (ticketing, automation, bots). Each channel picks which events it receives."
|
||||
docsPath={DOCS_PATHS.notifications}
|
||||
/>
|
||||
<div className="p-6 space-y-4">
|
||||
<ReadOnlyFieldset readOnly={readOnly}>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface OffboardingInProgressInfo {
|
||||
status: string; // "Queued" | "Initiated" | "InProgress" | "Completed" | "Failed"
|
||||
@@ -118,17 +120,13 @@ export default function OffboardingSection({
|
||||
<>
|
||||
{/* Danger Zone: Tenant Offboarding */}
|
||||
<div className="bg-white rounded-lg shadow border-2 border-red-200">
|
||||
<div className="p-6 border-b border-red-100 bg-red-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-red-900">Danger Zone</h2>
|
||||
<p className="text-sm text-red-600 mt-1">Irreversible and destructive actions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="danger"
|
||||
iconPath="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
title="Danger Zone"
|
||||
subtitle="Irreversible and destructive actions"
|
||||
docsPath={DOCS_PATHS.dangerZone}
|
||||
/>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface UnrestrictedModeSectionProps {
|
||||
unrestrictedMode: boolean;
|
||||
@@ -34,17 +36,13 @@ export default function UnrestrictedModeSection({
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<h2 className="text-lg font-semibold text-amber-900">Unrestricted Mode</h2>
|
||||
</div>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
Disable agent guardrails for advanced data collection
|
||||
</p>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="warning"
|
||||
iconPath="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
title="Unrestricted Mode"
|
||||
subtitle="Disable agent guardrails for advanced data collection"
|
||||
docsPath={DOCS_PATHS.unrestrictedMode}
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Active warning banner */}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { useAuth } from "../../../../contexts/AuthContext";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { api } from "@/lib/api";
|
||||
import { DocsLink } from "@/components/DocsLink";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
interface UsageRecord {
|
||||
userId: string;
|
||||
@@ -177,6 +179,7 @@ export function SectionMcpUsage() {
|
||||
>
|
||||
{loading ? "Loading..." : "Refresh"}
|
||||
</button>
|
||||
<DocsLink path={DOCS_PATHS.mcpUsage} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import SaveResetBar from "../../components/SaveResetBar";
|
||||
import ReadOnlyFieldset from "../../components/ReadOnlyFieldset";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
export function SectionContact() {
|
||||
const {
|
||||
@@ -17,17 +19,13 @@ export function SectionContact() {
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-blue-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Contact</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Where we reach you about the service</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="indigo"
|
||||
iconPath="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
title="Contact"
|
||||
subtitle="Where we reach you about the service"
|
||||
docsPath={DOCS_PATHS.contact}
|
||||
/>
|
||||
|
||||
<div className="p-6 space-y-4">
|
||||
<ReadOnlyFieldset readOnly={!canEditConfig}>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import { trialDaysLeft } from "@/lib/edition";
|
||||
import { PlanCards } from "@/components/plans/PlanCards";
|
||||
import { SITE_URL } from "@/utils/config";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
/**
|
||||
* Self-service Pro trial switch. The Pro feature set is not finalized yet, so the
|
||||
@@ -135,17 +137,13 @@ export function SectionPlan() {
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-indigo-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Plan</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Your current plan and what Pro adds</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="purple"
|
||||
iconPath="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
|
||||
title="Plan"
|
||||
subtitle="Your current plan and what Pro adds"
|
||||
docsPath={DOCS_PATHS.plan}
|
||||
/>
|
||||
|
||||
<div className="p-6">
|
||||
<PlanCards
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useTenantConfig } from "../../TenantConfigContext";
|
||||
import SaveResetBar from "../../components/SaveResetBar";
|
||||
import ReadOnlyFieldset from "../../components/ReadOnlyFieldset";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import { SectionCardHeader } from "@/components/SectionCardHeader";
|
||||
import { DOCS_PATHS } from "@/lib/docsPaths";
|
||||
|
||||
export function SectionSlaTargets() {
|
||||
const {
|
||||
@@ -23,21 +25,13 @@ export function SectionSlaTargets() {
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-blue-50">
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
SLA Targets
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
SLA targets and breach notification settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SectionCardHeader
|
||||
tone="indigo"
|
||||
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="SLA Targets"
|
||||
subtitle="SLA targets and breach notification settings"
|
||||
docsPath={DOCS_PATHS.slaTargets}
|
||||
/>
|
||||
<div className="p-6 space-y-8">
|
||||
<ReadOnlyFieldset readOnly={!canEditConfig}>
|
||||
<div className="space-y-8">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
|
||||
interface DocsLinkProps {
|
||||
/** Path below the published docs root, with a leading slash (e.g. "/reference/settings#notifications"). */
|
||||
path: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small "Read the docs" link that opens the matching customer documentation page in a new tab.
|
||||
* Neutral colouring so it sits on every section-header tone, including the red danger zone.
|
||||
*/
|
||||
export function DocsLink({ path, label = "Read the docs", className = "" }: DocsLinkProps) {
|
||||
return (
|
||||
<a
|
||||
href={`${DOCS_URL}${path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${label} (opens in a new tab)`}
|
||||
className={`inline-flex flex-shrink-0 items-center gap-1 whitespace-nowrap text-xs font-medium text-gray-500 hover:text-gray-900 hover:underline underline-offset-2 dark:text-gray-400 dark:hover:text-gray-100 ${className}`}
|
||||
>
|
||||
{label}
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { DocsLink } from "./DocsLink";
|
||||
|
||||
/**
|
||||
* Colour tones for section card headers. Every class string is literal so Tailwind can see it —
|
||||
* never assemble these from fragments. Tenant tones use the pastel 50-level gradient with grey
|
||||
* text; admin tones use the 100-level gradient, tinted text, and dark-mode variants.
|
||||
*/
|
||||
const TONES = {
|
||||
amber: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50",
|
||||
icon: "w-6 h-6 text-amber-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
violet: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-violet-50 to-purple-50",
|
||||
icon: "w-6 h-6 text-violet-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
emerald: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-emerald-50 to-teal-50",
|
||||
icon: "w-6 h-6 text-emerald-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
rose: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-rose-50 to-red-50",
|
||||
icon: "w-6 h-6 text-rose-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
indigo: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-blue-50",
|
||||
icon: "w-6 h-6 text-indigo-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
indigoPurple: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50",
|
||||
icon: "w-6 h-6 text-indigo-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
sky: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-blue-50",
|
||||
icon: "w-6 h-6 text-sky-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
skyIndigo: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-sky-50 to-indigo-50",
|
||||
icon: "w-6 h-6 text-sky-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
purple: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-purple-50 to-indigo-50",
|
||||
icon: "w-6 h-6 text-purple-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
green: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-green-50 to-emerald-50",
|
||||
icon: "w-6 h-6 text-green-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
cyan: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-cyan-50 to-teal-50",
|
||||
icon: "w-6 h-6 text-cyan-600",
|
||||
title: "text-xl font-semibold text-gray-900",
|
||||
subtitle: "text-sm text-gray-500 mt-1",
|
||||
},
|
||||
/** Amber gradient with amber text — guardrail-relaxing settings (Unrestricted Mode). */
|
||||
warning: {
|
||||
wrapper: "p-6 border-b border-gray-200 bg-gradient-to-r from-amber-50 to-orange-50",
|
||||
icon: "w-6 h-6 text-amber-600",
|
||||
title: "text-xl font-semibold text-amber-900",
|
||||
subtitle: "text-sm text-amber-700 mt-1",
|
||||
},
|
||||
/** Flat red — irreversible actions (Danger Zone). */
|
||||
danger: {
|
||||
wrapper: "p-6 border-b border-red-100 bg-red-50",
|
||||
icon: "w-6 h-6 text-red-600",
|
||||
title: "text-xl font-semibold text-red-900",
|
||||
subtitle: "text-sm text-red-600 mt-1",
|
||||
},
|
||||
adminIndigo: {
|
||||
wrapper: "p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-blue-100 dark:from-indigo-900/40 dark:to-blue-900/40",
|
||||
icon: "w-6 h-6 text-indigo-600 dark:text-indigo-400",
|
||||
title: "text-xl font-semibold text-indigo-900 dark:text-indigo-100",
|
||||
subtitle: "text-sm text-indigo-600 dark:text-indigo-300 mt-1",
|
||||
},
|
||||
adminIndigoPurple: {
|
||||
wrapper: "p-6 border-b border-indigo-200 dark:border-indigo-700 bg-gradient-to-r from-indigo-100 to-purple-100 dark:from-indigo-900/40 dark:to-purple-900/40",
|
||||
icon: "w-6 h-6 text-indigo-600 dark:text-indigo-400",
|
||||
title: "text-xl font-semibold text-indigo-900 dark:text-indigo-100",
|
||||
subtitle: "text-sm text-indigo-600 dark:text-indigo-300 mt-1",
|
||||
},
|
||||
adminEmerald: {
|
||||
wrapper: "p-6 border-b border-emerald-200 dark:border-emerald-700 bg-gradient-to-r from-emerald-100 to-teal-100 dark:from-emerald-900/40 dark:to-teal-900/40",
|
||||
icon: "w-6 h-6 text-emerald-600 dark:text-emerald-400",
|
||||
title: "text-xl font-semibold text-emerald-900 dark:text-emerald-100",
|
||||
subtitle: "text-sm text-emerald-600 dark:text-emerald-300 mt-1",
|
||||
},
|
||||
adminAmber: {
|
||||
wrapper: "p-6 border-b border-amber-200 dark:border-amber-700 bg-gradient-to-r from-amber-100 to-orange-100 dark:from-amber-900/40 dark:to-orange-900/40",
|
||||
icon: "w-6 h-6 text-amber-600 dark:text-amber-400",
|
||||
title: "text-xl font-semibold text-amber-900 dark:text-amber-100",
|
||||
subtitle: "text-sm text-amber-600 dark:text-amber-300 mt-1",
|
||||
},
|
||||
adminRed: {
|
||||
wrapper: "p-6 border-b border-red-200 dark:border-red-700 bg-gradient-to-r from-red-100 to-orange-100 dark:from-red-900/40 dark:to-orange-900/40",
|
||||
icon: "w-6 h-6 text-red-600 dark:text-red-400",
|
||||
title: "text-xl font-semibold text-red-900 dark:text-red-100",
|
||||
subtitle: "text-sm text-red-600 dark:text-red-300 mt-1",
|
||||
},
|
||||
adminTeal: {
|
||||
wrapper: "p-6 border-b border-teal-200 dark:border-teal-700 bg-gradient-to-r from-teal-100 to-cyan-100 dark:from-teal-900/40 dark:to-cyan-900/40",
|
||||
icon: "w-6 h-6 text-teal-600 dark:text-teal-400",
|
||||
title: "text-xl font-semibold text-teal-900 dark:text-teal-100",
|
||||
subtitle: "text-sm text-teal-600 dark:text-teal-300 mt-1",
|
||||
},
|
||||
adminPurple: {
|
||||
wrapper: "p-6 border-b border-purple-200 dark:border-purple-700 bg-gradient-to-r from-purple-100 to-violet-100 dark:from-purple-900/40 dark:to-violet-900/40",
|
||||
icon: "w-6 h-6 text-purple-600 dark:text-purple-400",
|
||||
title: "text-xl font-semibold text-purple-900 dark:text-purple-100",
|
||||
subtitle: "text-sm text-purple-600 dark:text-purple-300 mt-1",
|
||||
},
|
||||
adminSky: {
|
||||
wrapper: "p-6 border-b border-sky-200 dark:border-sky-700 bg-gradient-to-r from-sky-100 to-cyan-100 dark:from-sky-900/40 dark:to-cyan-900/40",
|
||||
icon: "w-6 h-6 text-sky-600 dark:text-sky-400",
|
||||
title: "text-xl font-semibold text-sky-900 dark:text-sky-100",
|
||||
subtitle: "text-sm text-sky-600 dark:text-sky-300 mt-1",
|
||||
},
|
||||
adminSlate: {
|
||||
wrapper: "p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-100 to-gray-100 dark:from-slate-900/40 dark:to-gray-900/40",
|
||||
icon: "w-6 h-6 text-slate-600 dark:text-slate-400",
|
||||
title: "text-xl font-semibold text-slate-900 dark:text-slate-100",
|
||||
subtitle: "text-sm text-slate-600 dark:text-slate-300 mt-1",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type SectionCardTone = keyof typeof TONES;
|
||||
|
||||
export interface SectionCardHeaderProps {
|
||||
tone: SectionCardTone;
|
||||
/** The `d` attribute of a single 24x24 Heroicons outline path. */
|
||||
iconPath: string;
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
/** Docs path below the published docs root; renders a "Read the docs" link on the right. */
|
||||
docsPath?: string;
|
||||
/** Badge, button, or toggle group rendered on the right, before the docs link. */
|
||||
trailing?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header strip of a settings/admin card: icon, title, subtitle on the left; optional trailing
|
||||
* controls and docs link on the right. Renders only the header — the card wrapper stays with the
|
||||
* caller so ids, refs, and borders remain where they are.
|
||||
*/
|
||||
export function SectionCardHeader({ tone, iconPath, title, subtitle, docsPath, trailing }: SectionCardHeaderProps) {
|
||||
const t = TONES[tone];
|
||||
const hasRight = trailing !== undefined || docsPath !== undefined;
|
||||
return (
|
||||
<div className={t.wrapper}>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<svg className={`${t.icon} flex-shrink-0`} fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={iconPath} />
|
||||
</svg>
|
||||
<div className="min-w-0">
|
||||
<h2 className={t.title}>{title}</h2>
|
||||
{subtitle !== undefined && <p className={t.subtitle}>{subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{hasRight && (
|
||||
<div className="flex flex-shrink-0 items-center gap-3">
|
||||
{trailing}
|
||||
{docsPath !== undefined && <DocsLink path={docsPath} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { SectionCardHeader } from "../SectionCardHeader";
|
||||
import { DocsLink } from "../DocsLink";
|
||||
import { DOCS_URL } from "@/utils/config";
|
||||
|
||||
const ICON = "M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4";
|
||||
|
||||
describe("SectionCardHeader", () => {
|
||||
it("renders tone classes, icon path, title and subtitle", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(SectionCardHeader, {
|
||||
tone: "amber",
|
||||
iconPath: ICON,
|
||||
title: "Diagnostics Package",
|
||||
subtitle: "Upload diagnostic files after enrollment.",
|
||||
}),
|
||||
);
|
||||
expect(html).toContain("from-amber-50 to-orange-50");
|
||||
expect(html).toContain("text-amber-600");
|
||||
expect(html).toContain(`d="${ICON}"`);
|
||||
expect(html).toContain("<h2 class=\"text-xl font-semibold text-gray-900\">Diagnostics Package</h2>");
|
||||
expect(html).toContain("Upload diagnostic files after enrollment.");
|
||||
// No right-hand group when neither docsPath nor trailing is given.
|
||||
expect(html).not.toContain("gap-3");
|
||||
});
|
||||
|
||||
it("renders the docs link as a new-tab link below DOCS_URL", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(SectionCardHeader, {
|
||||
tone: "sky",
|
||||
iconPath: ICON,
|
||||
title: "Notifications",
|
||||
docsPath: "/integrations/notifications",
|
||||
}),
|
||||
);
|
||||
expect(html).toContain(`href="${DOCS_URL}/integrations/notifications"`);
|
||||
expect(html).toContain('target="_blank"');
|
||||
expect(html).toContain('rel="noopener noreferrer"');
|
||||
expect(html).toContain("Read the docs");
|
||||
});
|
||||
|
||||
it("renders trailing content before the docs link", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(SectionCardHeader, {
|
||||
tone: "danger",
|
||||
iconPath: ICON,
|
||||
title: "Danger Zone",
|
||||
docsPath: "/reference/settings#danger-zone",
|
||||
trailing: createElement("span", { id: "badge" }, "Enabled"),
|
||||
}),
|
||||
);
|
||||
expect(html).toContain("bg-red-50");
|
||||
expect(html).toContain("text-red-900");
|
||||
expect(html.indexOf('id="badge"')).toBeGreaterThan(-1);
|
||||
expect(html.indexOf('id="badge"')).toBeLessThan(html.indexOf("Read the docs"));
|
||||
});
|
||||
|
||||
it("admin tones carry dark-mode variants", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(SectionCardHeader, { tone: "adminAmber", iconPath: ICON, title: "Alert Rules" }),
|
||||
);
|
||||
expect(html).toContain("dark:from-amber-900/40");
|
||||
expect(html).toContain("dark:text-amber-100");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DocsLink", () => {
|
||||
it("labels the new-tab behaviour for assistive tech", () => {
|
||||
const html = renderToStaticMarkup(createElement(DocsLink, { path: "/plans", label: "See plans" }));
|
||||
expect(html).toContain(`href="${DOCS_URL}/plans"`);
|
||||
expect(html).toContain('aria-label="See plans (opens in a new tab)"');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { DOCS_PATHS, DOCS_TOP_LEVEL_SEGMENTS } from "../docsPaths";
|
||||
|
||||
/**
|
||||
* GitBook publishes the first URL segment from the SUMMARY.md group heading, not the folder
|
||||
* name (`troubleshooting/` on disk is `/troubleshooting-and-support/` on the web). A path written
|
||||
* from the repo layout 404s silently in production — this test makes that a CI failure.
|
||||
*/
|
||||
describe("DOCS_PATHS", () => {
|
||||
const entries = Object.entries(DOCS_PATHS);
|
||||
|
||||
it("every path is root-relative, lowercase, with an optional single anchor", () => {
|
||||
for (const [key, path] of entries) {
|
||||
expect(path, key).toMatch(/^\/[a-z0-9-]+(\/[a-z0-9-]+)*(#[a-z0-9-]+)?$/);
|
||||
expect(path, key).not.toMatch(/\.md/);
|
||||
expect(path, key).not.toMatch(/--/);
|
||||
}
|
||||
});
|
||||
|
||||
it("every path starts with a published top-level segment", () => {
|
||||
const allowed = new Set<string>(DOCS_TOP_LEVEL_SEGMENTS);
|
||||
for (const [key, path] of entries) {
|
||||
const first = path.slice(1).split(/[/#]/)[0];
|
||||
expect(allowed.has(first), `${key}: "${first}" is not a published docs section`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not use on-disk folder names that GitBook renames", () => {
|
||||
for (const [key, path] of entries) {
|
||||
expect(path, key).not.toMatch(/^\/(troubleshooting|trust)\//);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Documentation page for each portal section that links to the customer docs, as a path below DOCS_URL.
|
||||
*
|
||||
* GitBook publishes the first path segment as the slugified SUMMARY.md group heading, not the
|
||||
* folder name: `troubleshooting/` → `/troubleshooting-and-support/`, `trust/` → `/trust-and-security/`.
|
||||
* Anchors are GitBook heading slugs (single hyphens). `docsPaths.test.ts` guards the shape.
|
||||
*/
|
||||
export const DOCS_PATHS = {
|
||||
// Settings → Tenant
|
||||
enrollmentDeviceValidation: "/reference/settings#enrollment-device-validation",
|
||||
hardwareWhitelist: "/reference/settings#hardware-whitelist",
|
||||
notifications: "/integrations/notifications",
|
||||
accessManagement: "/concepts/roles-and-permissions",
|
||||
slaTargets: "/portal-guide/sla-compliance#configuring-targets",
|
||||
plan: "/plans",
|
||||
contact: "/reference/settings#contact",
|
||||
bootstrapSessions: "/reference/bootstrap-script-and-tokens",
|
||||
mcpUsers: "/integrations/ai-integration-mcp",
|
||||
// Settings → Agent
|
||||
agentParameters: "/reference/settings#agent-parameters",
|
||||
agentCollectors: "/reference/settings#agent-collectors",
|
||||
agentAnalyzers: "/reference/settings#agent-analyzers",
|
||||
diagnosticsPackage: "/troubleshooting-and-support/diagnostics-and-log-collection",
|
||||
unrestrictedMode: "/reference/settings#unrestricted-mode-pro-plan-on-request",
|
||||
// Settings → Maintenance
|
||||
dataManagement: "/reference/settings#data-management",
|
||||
dangerZone: "/reference/settings#danger-zone",
|
||||
// Settings → Reporting
|
||||
mcpUsage: "/integrations/ai-integration-mcp#rate-limits-and-usage-plans",
|
||||
// Rules
|
||||
analyzeRules: "/rules/analyze-rules",
|
||||
gatherRules: "/rules/gather-rules",
|
||||
} as const;
|
||||
|
||||
/** Top-level URL segments GitBook publishes for docs.autopilotmonitor.com (SUMMARY.md groups + root pages). */
|
||||
export const DOCS_TOP_LEVEL_SEGMENTS = [
|
||||
"getting-started",
|
||||
"concepts",
|
||||
"portal-guide",
|
||||
"rules",
|
||||
"integrations",
|
||||
"reference",
|
||||
"trust-and-security",
|
||||
"troubleshooting-and-support",
|
||||
"changelog",
|
||||
"plans",
|
||||
] as const;
|
||||
Reference in New Issue
Block a user