feat(dashboard): group data-plane read surfaces + CodeEditor readOnly fix
Remediate the dashboard-coverage and editor findings from the 2026-07-11 audit.
Close the group-surface gap: a GroupTabBar plus read-only surfaces for the v1.2
group data plane the dashboard previously exposed only via the CLI — vars,
secrets (names + timestamps only, never values), shared collections (KV/docs/
files browsers), scripts, triggers, routes, suppressions, extension points,
dead-letters, and ownership. All server data renders through Svelte's escaped
interpolation (no `{@html}`); each tab hits its existing
`/api/v1/admin/groups/{id}/...` read endpoint with independent loading/empty
state.
B7 — the CodeEditor `readOnly` prop is now reactive via a CodeMirror
Compartment, fixing the race where an authorized user opening a script on a warm
SPA nav got a permanently read-only editor (role resolved after mount). The
reconfigure preserves editor content.
A viewer lacking a read cap now sees a calm "you don't have permission" note on
every tab (structured LoadError + classifyError) instead of a red error panel,
generalizing the one-off Secrets 403 handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
dashboard/src/lib/GroupTabBar.svelte
Normal file
88
dashboard/src/lib/GroupTabBar.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
/// Whether the current user can manage this group (instance owner/admin
|
||||
/// today). Gates the management-only tabs (Members, Secrets, Settings).
|
||||
canManage: boolean;
|
||||
/// Active tab id — the `?tab=` query param the main group page reads.
|
||||
currentTab: string;
|
||||
}
|
||||
|
||||
let { slug, canManage, currentTab }: Props = $props();
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
label: string;
|
||||
/// Management-only tabs (Members, Secrets, Settings) hide from a plain
|
||||
/// viewer. The read-only inspection tabs stay visible to any viewer;
|
||||
/// the backend still 403s the underlying calls if they can't read.
|
||||
manageOnly: boolean;
|
||||
}
|
||||
|
||||
// All tabs are in-page — the group page reads `?tab=` and renders the
|
||||
// matching section, loading its data lazily on activation.
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'overview', label: 'Overview', manageOnly: false },
|
||||
{ id: 'members', label: 'Members', manageOnly: true },
|
||||
{ id: 'vars', label: 'Vars', manageOnly: false },
|
||||
{ id: 'secrets', label: 'Secrets', manageOnly: true },
|
||||
{ id: 'collections', label: 'Collections', manageOnly: false },
|
||||
{ id: 'scripts', label: 'Scripts', manageOnly: false },
|
||||
{ id: 'triggers', label: 'Triggers', manageOnly: false },
|
||||
{ id: 'routes', label: 'Routes', manageOnly: false },
|
||||
{ id: 'suppressions', label: 'Suppressions', manageOnly: false },
|
||||
{ id: 'extension-points', label: 'Extension points', manageOnly: false },
|
||||
{ id: 'dead-letters', label: 'Dead-letters', manageOnly: false },
|
||||
{ id: 'ownership', label: 'Ownership', manageOnly: false },
|
||||
{ id: 'settings', label: 'Settings', manageOnly: true }
|
||||
];
|
||||
</script>
|
||||
|
||||
<nav class="tabbar" aria-label="Group sections">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{#if !tab.manageOnly || canManage}
|
||||
<a
|
||||
href="{base}/groups/{slug}?tab={tab.id}"
|
||||
class:active={tab.id === currentTab}
|
||||
aria-current={tab.id === currentTab ? 'page' : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.tabbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tabbar a {
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tabbar a:hover {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.tabbar a.active {
|
||||
color: #e2e8f0;
|
||||
border-bottom-color: #38bdf8;
|
||||
}
|
||||
|
||||
.tabbar a:focus-visible {
|
||||
outline: 2px solid #38bdf8;
|
||||
outline-offset: -2px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user