feat(admin): group integration toggles by category

Bucket the integration list under category headers (PSA, RMM,
Documentation, Security, …) following a canonical order, with
unrecognized categories appended after. Rows within a bucket sort
alphabetically by name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-05-03 11:03:54 -04:00
parent 344276751e
commit 370125d249

View file

@ -49,6 +49,27 @@ interface MergedRow {
setting: IntegrationSetting;
}
const CATEGORY_LABELS: Record<string, string> = {
psa: 'PSA',
rmm: 'RMM',
docs: 'Documentation',
security: 'Security',
backup: 'Backup',
network: 'Network',
identity: 'Identity',
mdm: 'MDM',
mail: 'Mail',
finance: 'Finance',
productivity: 'Productivity',
llm: 'LLM',
};
const CATEGORY_ORDER = [
'psa', 'rmm', 'docs', 'security', 'backup',
'network', 'identity', 'mdm', 'mail',
'finance', 'productivity', 'llm',
];
function liveStatusLight(s: IntegrationHealthItem['status']): StatusLightState {
if (s === 'ok') return 'ok';
if (s === 'auth_failed' || s === 'unreachable') return 'error';
@ -227,20 +248,49 @@ export default function IntegrationTogglesPage() {
/>
</div>
) : (
<ul className="divide-y divide-border">
{rows.map((row) => (
<IntegrationRow
key={row.key}
row={row}
pending={pending === row.key}
reasonValue={reasons[row.key] ?? ''}
onReasonChange={(v) =>
setReasons((prev) => ({ ...prev, [row.key]: v }))
}
onToggle={(next) => void toggle(row, next)}
/>
))}
</ul>
(() => {
// Bucket rows by category, preserving the canonical order.
const byCategory = new Map<string, MergedRow[]>();
for (const r of rows) {
const arr = byCategory.get(r.category) ?? [];
arr.push(r);
byCategory.set(r.category, arr);
}
const visibleCategories = CATEGORY_ORDER.filter((c) => byCategory.has(c));
// Catch any unexpected category not in the canonical order.
for (const c of byCategory.keys()) {
if (!visibleCategories.includes(c)) visibleCategories.push(c);
}
return (
<div className="divide-y divide-border">
{visibleCategories.map((category) => (
<div key={category}>
<p className="metric-label px-4 pt-3 pb-1">
{CATEGORY_LABELS[category] ?? category}
</p>
<ul className="divide-y divide-border/50">
{byCategory.get(category)!
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((row) => (
<IntegrationRow
key={row.key}
row={row}
pending={pending === row.key}
reasonValue={reasons[row.key] ?? ''}
onReasonChange={(v) =>
setReasons((prev) => ({ ...prev, [row.key]: v }))
}
onToggle={(next) => void toggle(row, next)}
/>
))}
</ul>
</div>
))}
</div>
);
})()
)}
</CardContent>
</Card>