Skip to content

Commit 1e7b734

Browse files
committed
feat: add run visibility and refactor permission system
1 parent ca22380 commit 1e7b734

26 files changed

Lines changed: 727 additions & 376 deletions

File tree

frontend/app/src/lib/api/client.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
NetworkFilters,
1616
NetworkUpdate,
1717
OutputFile,
18+
Visibility,
1819
PaginatedResponse,
1920
Workflow,
2021
} from "$lib/types.js";
@@ -126,7 +127,7 @@ export const networks = {
126127
async delete(id: string): Promise<void> {
127128
return request<void>(`/networks/${id}`, { method: 'DELETE' });
128129
},
129-
async updateVisibility(id: string, visibility: "public" | "private"): Promise<Network> {
130+
async updateVisibility(id: string, visibility: Visibility): Promise<Network> {
130131
return request<Network>(`/networks/${id}`, {
131132
method: 'PATCH',
132133
body: JSON.stringify({ visibility })
@@ -277,6 +278,12 @@ export const runs = {
277278
async remove(id: string): Promise<void> {
278279
return request<void>(`/runs/${id}`, { method: 'DELETE' });
279280
},
281+
async updateVisibility(id: string, visibility: Visibility): Promise<RunSummary> {
282+
return request<RunSummary>(`/runs/${id}`, {
283+
method: 'PATCH',
284+
body: JSON.stringify({ visibility })
285+
});
286+
},
280287
logsUrl(id: string): string {
281288
return `${API_BASE}/runs/${id}/logs`;
282289
},
@@ -370,6 +377,24 @@ export const admin = {
370377
return request<void>(`/admin/networks/${networkId}`, { method: 'DELETE' });
371378
},
372379

380+
async listRuns(skip = 0, limit = 100, filters: { visibility?: string; owner?: string } = {}): Promise<PaginatedResponse<RunSummary>> {
381+
let url = `/admin/runs?skip=${skip}&limit=${limit}`;
382+
if (filters.visibility) url += `&visibility=${filters.visibility}`;
383+
if (filters.owner) url += `&owner=${filters.owner}`;
384+
return request<PaginatedResponse<RunSummary>>(url);
385+
},
386+
387+
async updateRun(runId: string, updates: { visibility?: Visibility; user_id?: string }): Promise<Run> {
388+
return request<Run>(`/admin/runs/${runId}`, {
389+
method: 'PATCH',
390+
body: JSON.stringify(updates)
391+
});
392+
},
393+
394+
async deleteRun(runId: string): Promise<void> {
395+
return request<void>(`/admin/runs/${runId}`, { method: 'DELETE' });
396+
},
397+
373398
async getPermissions(): Promise<Record<string, unknown>> {
374399
return request<Record<string, unknown>>('/admin/permissions');
375400
},

frontend/app/src/lib/components/cells/VisibilityCell.svelte

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<script lang="ts">
22
import { Button } from '$lib/components/ui/button';
3-
import { Share2, Lock } from 'lucide-svelte';
3+
import { Globe, LockKeyhole } from 'lucide-svelte';
44
import * as Tooltip from '$lib/components/ui/tooltip';
5-
import type { Network } from '$lib/types.js';
5+
import type { Visibility } from '$lib/types.js';
66
7-
let { network, canEdit, onToggle }: {
8-
network: Network;
7+
let { item, canEdit, onToggle }: {
8+
item: { id: string; visibility: Visibility; owner?: { id: string } | null };
99
canEdit: boolean;
10-
onToggle: (id: string, visibility: 'public' | 'private') => void;
10+
onToggle: (id: string, visibility: Visibility) => void;
1111
} = $props();
1212
13-
const isSystem = $derived(!network.owner);
14-
const isPublic = $derived(network.visibility === 'public');
15-
const Icon = $derived(isPublic ? Share2 : Lock);
13+
const isSystem = $derived(!item.owner);
14+
const isPublic = $derived(item.visibility === 'public');
15+
const Icon = $derived(isPublic ? Globe : LockKeyhole);
1616
const iconClass = 'h-4 w-4 text-muted-foreground';
1717
</script>
1818

@@ -28,7 +28,7 @@
2828
variant="ghost"
2929
size="sm"
3030
class="h-8 w-8 p-0"
31-
onclick={() => onToggle(network.id, isPublic ? 'private' : 'public')}
31+
onclick={() => onToggle(item.id, isPublic ? 'private' : 'public')}
3232
{...props}
3333
>
3434
<Icon class={iconClass} />
@@ -41,7 +41,7 @@
4141
{/snippet}
4242
</Tooltip.Trigger>
4343
<Tooltip.Content>
44-
{isPublic ? 'Visible to all users' : 'Only visible to you'}
44+
{isPublic ? 'Visible to all users' : 'Only visible to the owner'}
4545
</Tooltip.Content>
4646
</Tooltip.Root>
4747
</div>

frontend/app/src/lib/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface Network {
3939
filename: string;
4040
file_path: string;
4141
file_size?: number;
42-
visibility: "public" | "private";
42+
visibility: Visibility;
4343
owner: User;
4444
source_run_id?: string;
4545
dimensions?: Record<string, number>;
@@ -52,6 +52,8 @@ export interface Network {
5252
updated_at?: string;
5353
}
5454

55+
export type Visibility = "public" | "private";
56+
5557
export interface BackendPublic {
5658
id: string;
5759
name: string;
@@ -89,6 +91,7 @@ export interface RunSummary {
8991
completed_at?: string;
9092
created_at: string;
9193
owner: User;
94+
visibility: Visibility;
9295
backend: BackendPublic;
9396
total_job_count?: number;
9497
jobs_finished?: number;
@@ -169,7 +172,7 @@ export interface NetworkFilters {
169172
}
170173

171174
export interface NetworkUpdate {
172-
visibility?: "public" | "private";
175+
visibility?: Visibility;
173176
user_id?: string;
174177
}
175178

frontend/app/src/routes/database/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { authStore } from '$lib/stores/auth.svelte.js';
1818
import EmptyState from '$lib/components/EmptyState.svelte';
1919
import TableSkeleton from '$lib/components/TableSkeleton.svelte';
20-
import type { Network as NetworkType, User, NetworkUpdate, ApiError } from '$lib/types.js';
20+
import type { Network as NetworkType, User, NetworkUpdate, ApiError, Visibility } from '$lib/types.js';
2121
import type { FilterState, FilterCategory } from '$lib/components/ui/filter-dialog';
2222
import type { ColumnDef, SortingState, VisibilityState, Row } from '@tanstack/table-core';
2323
@@ -217,7 +217,7 @@
217217
}
218218
}
219219
220-
async function handleVisibilityToggle(networkId: string, newVisibility: "public" | "private") {
220+
async function handleVisibilityToggle(networkId: string, newVisibility: Visibility) {
221221
if (updatingVisibilityId) return;
222222
updatingVisibilityId = networkId;
223223
try {

frontend/app/src/routes/database/components/columns.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import VisibilityCell from '$lib/components/cells/VisibilityCell.svelte';
99
import { Lock, Globe, Trash2, UserRoundCog } from 'lucide-svelte';
1010
import OwnerCell from '$lib/components/OwnerCell.svelte';
1111
import type { ColumnDef } from '@tanstack/table-core';
12-
import type { Network, NetworkTag, TagType, TagColor } from '$lib/types.js';
12+
import type { Network, NetworkTag, TagType, TagColor, Visibility } from '$lib/types.js';
1313

1414
interface DatabaseColumnsHelpers {
1515
getDirectoryPath: (fullPath: string | null | undefined) => string;
@@ -20,7 +20,7 @@ interface DatabaseColumnsHelpers {
2020
handleDelete: (id: string) => void;
2121
toggleComponentsExpanded: (id: string) => void;
2222
getExpandedComponents: () => Set<string>;
23-
handleVisibilityToggle: (id: string, visibility: "public" | "private") => void;
23+
handleVisibilityToggle: (id: string, visibility: Visibility) => void;
2424
canEditNetwork: (network: Network) => boolean;
2525
authEnabled: boolean;
2626
handleOwnerChange?: (network: Network) => void;
@@ -126,7 +126,7 @@ export const createColumns = (helpers: DatabaseColumnsHelpers): ColumnDef<Networ
126126
cell: (info: { row: { original: Network } }) => {
127127
const network = info.row.original;
128128
return renderComponent(VisibilityCell, {
129-
network,
129+
item: network,
130130
canEdit: canEditNetwork(network),
131131
onToggle: handleVisibilityToggle
132132
});

frontend/app/src/routes/runs/+page.svelte

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { formatRelativeTime, saveTablePref, buildOwnerOptions } from '$lib/utils.js';
88
import { restoreTableState, buildTableURL, filtersToAPI, clampPage } from '$lib/table-url-state.js';
99
import { RUN_FINAL_STATUSES } from '$lib/types.js';
10-
import type { RunSummary, User, BackendPublic, ApiError } from '$lib/types.js';
10+
import type { RunSummary, User, BackendPublic, ApiError, Visibility } from '$lib/types.js';
1111
import type { FilterState, FilterCategory } from '$lib/components/ui/filter-dialog';
1212
import type { SortingState, VisibilityState } from '@tanstack/table-core';
1313
import FilterBar from '$lib/components/FilterBar.svelte';
@@ -26,6 +26,7 @@
2626
let totalRuns = $state(0);
2727
let cancellingId = $state<string | null>(null);
2828
let removingId = $state<string | null>(null);
29+
let updatingVisibilityId = $state<string | null>(null);
2930
3031
const FILTER_KEYS = ['statuses', 'workflows', 'owners', 'git_refs', 'configfiles', 'backends'] as const;
3132
let filters = $state({ statuses: new Set<string>(), workflows: new Set<string>(), owners: new Set<string>(), git_refs: new Set<string>(), configfiles: new Set<string>(), backends: new Set<string>() });
@@ -91,16 +92,25 @@
9192
{ key: 'backends', label: 'Backend', options: availableBackends.map(b => ({ id: b.id, label: b.name })) },
9293
]);
9394
95+
function canEditRun(run: RunSummary): boolean {
96+
if (!authStore.user) return false;
97+
if (authStore.user.permissions?.includes('runs:manage_all')) return true;
98+
return run.owner?.id === authStore.user.id;
99+
}
100+
94101
const columns = $derived.by(() => {
95102
const authEnabled = authStore.authEnabled ?? false;
96103
return createColumns({
97104
formatRelativeTime,
98105
handleCancel,
99106
handleRemove,
100107
handleRerun,
108+
handleVisibilityToggle,
109+
canEditRun,
101110
authEnabled,
102111
getCancellingId: () => cancellingId,
103112
getRemovingId: () => removingId,
113+
getUpdatingVisibilityId: () => updatingVisibilityId,
104114
getTick: () => tick
105115
});
106116
});
@@ -206,6 +216,19 @@
206216
}
207217
}
208218
219+
async function handleVisibilityToggle(runId: string, visibility: Visibility) {
220+
if (updatingVisibilityId) return;
221+
updatingVisibilityId = runId;
222+
try {
223+
await runs.updateVisibility(runId, visibility);
224+
await loadRuns(true);
225+
} catch (err) {
226+
if (!(err as ApiError).cancelled) toast.error((err as Error).message);
227+
} finally {
228+
updatingVisibilityId = null;
229+
}
230+
}
231+
209232
async function handleRemove(runId: string) {
210233
if (removingId) return;
211234
if (!confirm('Are you sure you want to remove this run? This will delete all associated files and cannot be undone.')) {

frontend/app/src/routes/runs/[id]/+page.svelte

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import type { Run, ApiError, OutputFile, RunNetwork, Workflow } from '$lib/types.js';
99
import { Button } from '$lib/components/ui/button';
1010
import { Skeleton } from '$lib/components/ui/skeleton';
11-
import { Terminal, RotateCw, X, Trash2, Loader2, MoreVertical, Settings2, ChevronRight, ExternalLink, FolderArchive, GitBranch, Clock, Calendar, Server } from 'lucide-svelte';
11+
import { Terminal, RotateCw, X, Trash2, Loader2, MoreVertical, Settings2, ChevronRight, ExternalLink, FolderArchive, GitBranch, Clock, Calendar, Server, Globe, LockKeyhole } from 'lucide-svelte';
12+
import { authStore } from '$lib/stores/auth.svelte.js';
1213
import { breadcrumbStore } from '$lib/stores/breadcrumb.svelte.js';
1314
import OutputFilesTree from '../components/OutputFilesTree.svelte';
1415
import WorkflowSection from '../components/WorkflowSection.svelte';
@@ -27,6 +28,7 @@
2728
let rerunning = $state(false);
2829
let cancelling = $state(false);
2930
let removing = $state(false);
31+
let togglingVisibility = $state(false);
3032
let configOpen = $state(false);
3133
let logsOpen = $state(true);
3234
@@ -101,7 +103,7 @@
101103
}
102104
});
103105
104-
const actionBusy = $derived(cancelling || rerunning || removing);
106+
const actionBusy = $derived(cancelling || rerunning || removing || togglingVisibility);
105107
106108
const duration = $derived.by(() => {
107109
if (!isTerminal) tick; // reference tick to force re-evaluation
@@ -283,6 +285,19 @@
283285
goto('/runs');
284286
});
285287
288+
const canEditRun = $derived(
289+
run && authStore.user && (
290+
authStore.user.permissions?.includes('runs:manage_all') ||
291+
run.owner?.id === authStore.user.id
292+
)
293+
);
294+
295+
const handleToggleVisibility = () => runAction(v => togglingVisibility = v, async () => {
296+
const newVis = run!.visibility === 'public' ? 'private' : 'public';
297+
await runs.updateVisibility(run!.id, newVis);
298+
run = await runs.get(runId);
299+
});
300+
286301
function scrollToBottom() {
287302
requestAnimationFrame(() => {
288303
if (logContainer) {
@@ -356,6 +371,17 @@
356371
Rerun
357372
</DropdownMenu.Item>
358373
{/if}
374+
{#if canEditRun}
375+
<DropdownMenu.Item onclick={handleToggleVisibility} disabled={actionBusy}>
376+
{#if run?.visibility === 'public'}
377+
<LockKeyhole class="h-4 w-4 mr-2" />
378+
Make private
379+
{:else}
380+
<Globe class="h-4 w-4 mr-2" />
381+
Make public
382+
{/if}
383+
</DropdownMenu.Item>
384+
{/if}
359385
<DropdownMenu.Separator />
360386
<DropdownMenu.Item onclick={handleRemove} disabled={actionBusy} class="text-destructive focus:text-destructive">
361387
<Trash2 class="h-4 w-4 mr-2" />
@@ -391,6 +417,18 @@
391417
<Server class="h-3.5 w-3.5" />
392418
<span>{run.backend.name}</span>
393419
</div>
420+
{#if authStore.authEnabled}
421+
<div class="h-4 w-px bg-border"></div>
422+
<div class="flex items-center gap-1.5">
423+
{#if run.visibility === 'public'}
424+
<Globe class="h-3.5 w-3.5" />
425+
<span>Public</span>
426+
{:else}
427+
<LockKeyhole class="h-3.5 w-3.5" />
428+
<span>Private</span>
429+
{/if}
430+
</div>
431+
{/if}
394432
{#if run.networks.length > 0}
395433
<div class="h-4 w-px bg-border"></div>
396434
<div class="flex items-center gap-1.5">

0 commit comments

Comments
 (0)