diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte index 71dda5e1d3..797c06b64f 100644 --- a/frontend/src/lib/components/DBManagerDrawer.svelte +++ b/frontend/src/lib/components/DBManagerDrawer.svelte @@ -58,6 +58,7 @@ if (!workspace || !datatable) return undefined try { return { + workspace, datatable, ...(await listUsableDatatableRoles(workspace, datatable)) } @@ -65,16 +66,24 @@ // Never leave the drawer waiting on this: fall back to the // unpermissioned shape so it opens and the server picks the role. console.error('Failed to load datatable roles:', e) - return { datatable, permissioned: false, roles: [], default_role: ADMIN_DATATABLE_ROLE } + return { + workspace, + datatable, + permissioned: false, + roles: [], + default_role: ADMIN_DATATABLE_ROLE + } } } ) - // A resource keeps its previous value while refetching, and roles are per data - // table: settling from the last one's answer would connect to the new data - // table as a role it may not even have. + // A resource keeps its previous value while refetching, and roles are per data table of one + // workspace: settling from another answer would connect to this data table as a role it may + // not even have. const rolesOfCurrent = $derived( - usableRoles.current?.datatable === selectedDatatable ? usableRoles.current : undefined + usableRoles.current?.datatable === selectedDatatable && usableRoles.current?.workspace === ws + ? usableRoles.current + : undefined ) // Nothing that connects runs until the role is settled: a first round sent without a role diff --git a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte index c3fd043198..7ccf89a275 100644 --- a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte @@ -85,6 +85,7 @@ if (!isOpen || !workspace || !datatable) return undefined try { return { + workspace, datatable, ...(await listUsableDatatableRoles(workspace, datatable)) } @@ -93,6 +94,7 @@ // that is refused. console.error('Failed to load datatable roles:', e) return { + workspace, datatable, permissioned: false, roles: [] as string[], @@ -102,9 +104,12 @@ } ) - // A resource keeps its previous value while it refetches, and roles are per data table. + // A resource keeps its previous value while it refetches, and roles are per data table of one + // workspace: a fork and its parent both have a `main`, with their own roles on it. const rolesOfCurrent = $derived( - usableRoles.current?.datatable === selectedDatatable ? usableRoles.current : undefined + usableRoles.current?.datatable === selectedDatatable && usableRoles.current?.workspace === opWs + ? usableRoles.current + : undefined ) // Mounting the manager fires its first queries, so it waits for the role: a round sent diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index e9b0dfa786..9a5eb001ec 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -747,6 +747,8 @@ data = next aiChatManager.datatableCreationPolicy = { ...aiChatManager.datatableCreationPolicy, + // As on load: data that names no data table leaves nothing to create tables in. + enabled: next.datatable !== undefined, datatable: next.datatable, schema: next.schema, roles: next.roles diff --git a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte index 2851b7384a..0afef7744d 100644 --- a/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppTemplatePicker.svelte @@ -86,10 +86,13 @@ let selectedRole = $state(undefined) // Every reader waits for an answer stamped with the current selection: until then `current` - // belongs to the previous data table or role. + // belongs to the previous workspace, data table or role. // A data table whose name cannot carry a role in a reference is used as its default one. + const rolesAnswered = $derived( + roles.current.workspace === opWs && roles.current.datatable === selectedDatatable + ) const loadedRoles = $derived( - roles.current.datatable === selectedDatatable && + rolesAnswered && selectedDatatable !== undefined && datatableNameTakesRole(selectedDatatable) ? roles.current.roles @@ -114,8 +117,7 @@ const hasNoDatatables = $derived(availableDatatables?.length === 0) const rolesSettled = $derived( - hasNoDatatables || - (selectedDatatable !== undefined && roles.current.datatable === selectedDatatable) + hasNoDatatables || (selectedDatatable !== undefined && rolesAnswered) ) // A role is picked on one data table: two data tables can both define an `analyst` that @@ -123,7 +125,7 @@ let rolesPickedOn = $state(undefined) $effect(() => { const loaded = roles.current - if (loaded.datatable !== selectedDatatable) return + if (!rolesAnswered) return const switched = untrack(() => rolesPickedOn) !== selectedDatatable const current = untrack(() => selectedRole) if (switched || current === undefined || !loaded.roles.includes(current)) { @@ -173,6 +175,7 @@ hasNoDatatables || (rolesSettled && selectedDatatable !== undefined && + access.current.workspace === opWs && access.current.datatable === selectedDatatable && access.current.role === effectiveRole) ) diff --git a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts index 731b8120a6..566aac9484 100644 --- a/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts +++ b/frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts @@ -40,8 +40,9 @@ export function createDatatablesResource(getWorkspace: () => string | undefined) } export type DatatableRoles = { - /** The data table this answers for: while a switch is in flight, `current` still holds the - * previous one's roles, which say nothing about the one now selected. */ + /** The workspace and data table this answers for: while a switch is in flight, `current` still + * holds the previous ones' roles, and a fork and its parent both have a `main` of their own. */ + workspace: string | undefined datatable: string | undefined /** Whether the data table is under roles. Without roles `roles` is empty because there is * nothing to pick, which is not the same as a permissioned one this caller may use no role of. */ @@ -61,6 +62,7 @@ export function createRolesResource( getWorkspace: () => string | undefined = () => get(workspaceStore) ) { const initialValue: DatatableRoles = { + workspace: undefined, datatable: undefined, permissioned: false, failed: false, @@ -71,7 +73,11 @@ export function createRolesResource( () => [getDatatable() ?? '', getWorkspace() ?? ''] as const, latestOnly( async ([datatableName, workspace]: readonly [string, string]): Promise => { - const empty = { ...initialValue, datatable: datatableName || undefined } + const empty = { + ...initialValue, + workspace: workspace || undefined, + datatable: datatableName || undefined + } if (!datatableName || !workspace) return empty try { const res = await listUsableDatatableRoles(workspace, datatableName) @@ -94,8 +100,9 @@ export function createRolesResource( } export type DatatableAccess = { - /** What this answers for. Until both match the selection, the schemas and the right to - * create one belong to another data table or another role. */ + /** What this answers for. Until all three match the selection, the schemas and the right to + * create one belong to another workspace, data table or role. */ + workspace: string | undefined datatable: string | undefined role: string | undefined /** The request failed, or the server kept the entry with an error (a role this caller may @@ -119,6 +126,7 @@ export function createDatatableAccessResource( getReady: () => boolean = () => true ) { const initialValue: DatatableAccess = { + workspace: undefined, datatable: undefined, role: undefined, failed: false, @@ -137,6 +145,7 @@ export function createDatatableAccessResource( ]): Promise => { const asked = { ...initialValue, + workspace: workspace || undefined, datatable: datatable || undefined, role: role || undefined }