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:
parent
344276751e
commit
370125d249
1 changed files with 64 additions and 14 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue