diff --git a/web-admin/src/features/authentication/AvatarButton.svelte b/web-admin/src/features/authentication/AvatarButton.svelte index cb1b2f2b155..95dc0bb5d04 100644 --- a/web-admin/src/features/authentication/AvatarButton.svelte +++ b/web-admin/src/features/authentication/AvatarButton.svelte @@ -1,4 +1,19 @@ + + + +
+ + {#if organization} + + {/if} + +
+ {#if $user.isSuccess} + {#if $user.data?.user} + + {:else} + + {/if} + {/if} +
+
diff --git a/web-admin/src/features/navigation/TopNavigationBar.svelte b/web-admin/src/features/projects/ProjectHeader.svelte similarity index 54% rename from web-admin/src/features/navigation/TopNavigationBar.svelte rename to web-admin/src/features/projects/ProjectHeader.svelte index fbe7460e134..93ac6c359f5 100644 --- a/web-admin/src/features/navigation/TopNavigationBar.svelte +++ b/web-admin/src/features/projects/ProjectHeader.svelte @@ -5,7 +5,6 @@ import ShareDashboardPopover from "@rilldata/web-admin/features/dashboards/share/ShareDashboardPopover.svelte"; import ShareProjectPopover from "@rilldata/web-admin/features/projects/user-management/ShareProjectPopover.svelte"; import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token"; - import Rill from "@rilldata/web-common/components/icons/Rill.svelte"; import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte"; import type { PathOption } from "@rilldata/web-common/components/navigation/breadcrumbs/types"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; @@ -14,16 +13,18 @@ import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte"; import { useExplore } from "@rilldata/web-common/features/explores/selectors"; import { featureFlags } from "@rilldata/web-common/features/feature-flags"; - import { runtimeClientStore } from "@rilldata/web-common/runtime-client/v2"; - import RuntimeContextBridge from "@rilldata/web-common/runtime-client/v2/RuntimeContextBridge.svelte"; - import { readable } from "svelte/store"; + import Header from "@rilldata/web-common/layout/header/Header.svelte"; + import HeaderLogo from "@rilldata/web-common/layout/header/HeaderLogo.svelte"; + import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; + import type { V1ProjectPermissions } from "../../client"; import { createAdminServiceGetCurrentUser, createAdminServiceGetDeploymentCredentials, - createAdminServiceListOrganizations as listOrgs, - createAdminServiceListProjectsForOrganization as listProjects, - type V1Organization, } from "../../client"; + import { + useBreadcrumbOrgPaths, + useBreadcrumbProjectPaths, + } from "../navigation/breadcrumb-selectors"; import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte"; import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore"; import CreateAlert from "../alerts/CreateAlert.svelte"; @@ -37,14 +38,13 @@ import { isCanvasDashboardPage, isMetricsExplorerPage, - isOrganizationPage, isProjectPage, isPublicURLPage, - } from "./nav-utils"; + } from "../navigation/nav-utils"; - export let createMagicAuthTokens: boolean; - export let manageProjectAdmins: boolean; - export let manageProjectMembers: boolean; + export let organization: string; + export let project: string; + export let projectPermissions: V1ProjectPermissions; export let manageOrgAdmins: boolean; export let manageOrgMembers: boolean; export let readProjects: boolean; @@ -52,35 +52,26 @@ export let organizationLogoUrl: string | undefined; const user = createAdminServiceGetCurrentUser(); + const runtimeClient = useRuntimeClient(); const { alerts: alertsFlag, dimensionSearch, dashboardChat, stickyDashboardState, } = featureFlags; - // TopNavigationBar renders in the root layout, ABOVE RuntimeProvider. - // Subscribe to the global store so we reactively get the client when - // RuntimeProvider mounts on project pages. - $: runtimeClient = $runtimeClientStore; - - $: instanceId = runtimeClient?.instanceId ?? ""; - // These can be undefined $: ({ - params: { organization, project, dashboard, alert, report }, + params: { dashboard, alert, report }, } = $page); - $: onProjectPage = isProjectPage($page); $: onAlertPage = !!alert; $: onReportPage = !!report; + $: onProjectPage = isProjectPage($page); $: onMetricsExplorerPage = isMetricsExplorerPage($page); $: onCanvasDashboardPage = isCanvasDashboardPage($page); $: onPublicURLPage = isPublicURLPage($page); - $: onOrgPage = isOrganizationPage($page); // When "View As" is active, fetch deployment credentials for the mocked user. - // TanStack Query deduplicates by query key, so if the project layout already - // ran this query, we get instant cache hits with zero extra network calls. $: mockedUserId = $viewAsUserStore?.id; $: mockedCredentialsQuery = createAdminServiceGetDeploymentCredentials( @@ -107,100 +98,30 @@ ); // Use effective permissions when "View As" is active (from server) - // Otherwise fall back to the props passed from the root layout $: effectiveManageProjectMembers = $mockedProjectQuery.data?.projectPermissions?.manageProjectMembers ?? - manageProjectMembers; + projectPermissions.manageProjectMembers; $: effectiveCreateMagicAuthTokens = $mockedProjectQuery.data?.projectPermissions?.createMagicAuthTokens ?? - createMagicAuthTokens; + projectPermissions.createMagicAuthTokens; $: loggedIn = !!$user.data?.user; $: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/"; - $: logoUrl = organizationLogoUrl; - - $: organizationQuery = listOrgs( - { pageSize: 100 }, - { - query: { - enabled: !!$user.data?.user, - retry: 2, - refetchOnMount: true, - }, - }, - ); - $: projectsQuery = listProjects( + $: orgPathsQuery = useBreadcrumbOrgPaths( + loggedIn, organization, - { - pageSize: 100, - }, - { - query: { - enabled: !!organization && readProjects, - retry: 2, - refetchOnMount: true, - }, - }, + planDisplayName, ); + $: projectPathsQuery = useBreadcrumbProjectPaths(organization, readProjects); + $: visualizationsQuery = useDashboards(runtimeClient); + $: alertsQuery = useAlerts(runtimeClient, onAlertPage); + $: reportsQuery = useReports(runtimeClient, onReportPage); - // These queries only run when inside a RuntimeProvider (project pages). - // On org-level pages runtimeClient is null; use a no-op store so $-subscriptions don't crash. - const _noopQuery = readable({ data: undefined }) as any; - $: visualizationsQuery = runtimeClient - ? useDashboards(runtimeClient) - : _noopQuery; - $: alertsQuery = runtimeClient - ? useAlerts(runtimeClient, onAlertPage) - : _noopQuery; - $: reportsQuery = runtimeClient - ? useReports(runtimeClient, onReportPage) - : _noopQuery; - - $: organizations = $organizationQuery.data?.organizations ?? []; - $: projects = $projectsQuery.data?.projects ?? []; $: visualizations = $visualizationsQuery.data ?? []; $: alerts = $alertsQuery.data?.resources ?? []; $: reports = $reportsQuery.data?.resources ?? []; - $: organizationPaths = { - options: createOrgPaths(organizations, organization, planDisplayName), - }; - - function createOrgPaths( - organizations: V1Organization[], - viewingOrg: string | undefined, - planDisplayName: string, - ) { - const pathMap = new Map(); - - organizations.forEach(({ name, displayName }) => { - pathMap.set(name.toLowerCase(), { - label: displayName || name, - pill: planDisplayName, - }); - }); - - if (!viewingOrg) return pathMap; - - if (!pathMap.has(viewingOrg.toLowerCase())) { - pathMap.set(viewingOrg.toLowerCase(), { - label: viewingOrg, - pill: planDisplayName, - }); - } - - return pathMap; - } - - $: projectPaths = { - options: projects.reduce( - (map, { name }) => - map.set(name.toLowerCase(), { label: name, preloadData: false }), - new Map(), - ), - }; - $: visualizationPaths = { options: [...visualizations] .sort((a, b) => { @@ -249,17 +170,16 @@ }; $: pathParts = [ - organizationPaths, - projectPaths, + { options: $orgPathsQuery.data ?? new Map() }, + { options: $projectPathsQuery.data ?? new Map() }, visualizationPaths, report ? reportPaths : alert ? alertPaths : null, ]; - $: exploreQuery = runtimeClient - ? useExplore(runtimeClient, dashboard, { - enabled: !!instanceId && !!dashboard && !!onMetricsExplorerPage, - }) - : _noopQuery; + $: exploreQuery = useExplore(runtimeClient, dashboard, { + enabled: + !!runtimeClient.instanceId && !!dashboard && !!onMetricsExplorerPage, + }); $: exploreSpec = $exploreQuery.data?.explore?.explore?.state?.validSpec; $: isDashboardValid = !!exploreSpec; $: hasUserAccess = $user.isSuccess && $user.data.user && !onPublicURLPage; @@ -271,96 +191,76 @@ $: currentPath = [organization, project, dashboard, report || alert]; -
- - - {#if logoUrl} - logo - {:else} - - {/if} - +
+ {#if onPublicURLPage} {:else if organization} {/if} -
{#if $viewAsUserStore} {/if} - - {#if onProjectPage && effectiveManageProjectMembers} {/if} - {#if runtimeClient} - {#key runtimeClient} - - {#if onMetricsExplorerPage && isDashboardValid} - {#if exploreSpec} - {#key dashboard} - - - {#if $dimensionSearch && ready} - - {/if} - {#if $dashboardChat && !onPublicURLPage} - - {/if} - {#if hasUserAccess} - - {#if $alertsFlag} - - {/if} - - {/if} - - {/key} - {/if} - {/if} - {#if onCanvasDashboardPage && hasUserAccess} + {#if onMetricsExplorerPage && isDashboardValid} + {#if exploreSpec} + {#key dashboard} + + + {#if $dimensionSearch && ready} + + {/if} {#if $dashboardChat && !onPublicURLPage} {/if} - - - {/if} - - {/key} + {#if hasUserAccess} + + {#if $alertsFlag} + + {/if} + + {/if} + + {/key} + {/if} {/if} + + {#if onCanvasDashboardPage && hasUserAccess} + {#if $dashboardChat && !onPublicURLPage} + + {/if} + + + {/if} + {#if $user.isSuccess} - {#if $user.data && $user.data.user} + {#if $user.data?.user} {:else} {/if} {/if}
-
+ diff --git a/web-admin/src/features/projects/SlimProjectHeader.svelte b/web-admin/src/features/projects/SlimProjectHeader.svelte new file mode 100644 index 00000000000..db98cc36d41 --- /dev/null +++ b/web-admin/src/features/projects/SlimProjectHeader.svelte @@ -0,0 +1,51 @@ + + +
+ + + +
+ {#if $user.isSuccess} + {#if $user.data?.user} + + {:else} + + {/if} + {/if} +
+
diff --git a/web-admin/src/features/projects/status/overview/ProjectGlobalStatusIndicator.svelte b/web-admin/src/features/projects/status/overview/ProjectGlobalStatusIndicator.svelte index 2ef898c7ccc..b51b49c402e 100644 --- a/web-admin/src/features/projects/status/overview/ProjectGlobalStatusIndicator.svelte +++ b/web-admin/src/features/projects/status/overview/ProjectGlobalStatusIndicator.svelte @@ -5,66 +5,52 @@ import LoadingSpinner from "@rilldata/web-common/components/icons/LoadingSpinner.svelte"; import { useProjectParser } from "@rilldata/web-common/features/entity-management/resource-selectors"; import { createRuntimeServiceListResources } from "@rilldata/web-common/runtime-client"; - import { runtimeClientStore } from "@rilldata/web-common/runtime-client/v2"; + import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { useQueryClient } from "@tanstack/svelte-query"; import { useProjectDeployment } from "../selectors"; const queryClient = useQueryClient(); + const runtimeClient = useRuntimeClient(); export let organization: string; export let project: string; - $: runtimeClient = $runtimeClientStore; - $: projectDeployment = useProjectDeployment(organization, project); $: ({ data: deployment } = $projectDeployment); $: isDeploymentNotOk = deployment?.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING; - $: hasResourceErrorsQuery = runtimeClient - ? createRuntimeServiceListResources( - runtimeClient, - {}, - { - query: { - select: (data) => { - return ( - data.resources.filter( - (resource) => !!resource.meta.reconcileError, - ).length > 0 - ); - }, - refetchOnMount: true, - refetchOnWindowFocus: true, - }, + $: hasResourceErrorsQuery = createRuntimeServiceListResources( + runtimeClient, + {}, + { + query: { + select: (data) => { + return ( + data.resources.filter((resource) => !!resource.meta.reconcileError) + .length > 0 + ); }, - ) - : null; + refetchOnMount: true, + refetchOnWindowFocus: true, + }, + }, + ); $: ({ data: hasResourceErrors, error: hasResourceErrorsError, isLoading: hasResourceErrorsLoading, - } = $hasResourceErrorsQuery ?? { - data: undefined, - error: undefined, - isLoading: true, - }); + } = $hasResourceErrorsQuery); - $: projectParserQuery = runtimeClient - ? useProjectParser(queryClient, runtimeClient, { - refetchOnMount: true, - refetchOnWindowFocus: true, - }) - : null; + $: projectParserQuery = useProjectParser(queryClient, runtimeClient, { + refetchOnMount: true, + refetchOnWindowFocus: true, + }); $: ({ data: projectParserData, error: projectParserError, isLoading: projectParserLoading, - } = $projectParserQuery ?? { - data: undefined, - error: undefined, - isLoading: true, - }); + } = $projectParserQuery); $: hasParseErrors = projectParserData?.projectParser.state.parseErrors.length > 0; diff --git a/web-admin/src/routes/+layout.svelte b/web-admin/src/routes/+layout.svelte index 0f72e08a581..76ba904d827 100644 --- a/web-admin/src/routes/+layout.svelte +++ b/web-admin/src/routes/+layout.svelte @@ -25,7 +25,7 @@ import { type Query, QueryClientProvider } from "@tanstack/svelte-query"; import { onMount } from "svelte"; import ErrorBoundary from "../components/errors/ErrorBoundary.svelte"; - import TopNavigationBar from "../features/navigation/TopNavigationBar.svelte"; + import OrgHeader from "../features/organizations/OrgHeader.svelte"; import "@rilldata/web-common/app.css"; import { themeControl } from "@rilldata/web-common/features/themes/theme-control"; import { getThemedLogoUrl } from "@rilldata/web-admin/features/themes/organization-logo"; @@ -34,7 +34,6 @@ export let data; $: ({ - projectPermissions, organizationPermissions, organization: organizationObj, planDisplayName, @@ -148,13 +147,8 @@ {#if !hideBillingManager} {/if} - {#if !isEmbed && !hideTopBar} - { const userQuery = await queryClient.fetchQuery({ queryKey: getAdminServiceGetCurrentUserQueryKey(), queryFn: () => adminServiceGetCurrentUser(), + staleTime: 5 * 60 * 1000, // 5 minutes; prevents refetches on every navigation/hover }); user = userQuery.user; } catch (e) { diff --git a/web-admin/src/routes/-/embed/+layout.svelte b/web-admin/src/routes/-/embed/+layout.svelte index 4aebabf036f..adac66b2b5f 100644 --- a/web-admin/src/routes/-/embed/+layout.svelte +++ b/web-admin/src/routes/-/embed/+layout.svelte @@ -6,7 +6,7 @@ isDifferentDashboard, } from "@rilldata/web-admin/features/embeds/embed-route-utils.ts"; import initEmbedPublicAPI from "@rilldata/web-admin/features/embeds/init-embed-public-api.ts"; - import TopNavigationBarEmbed from "@rilldata/web-admin/features/embeds/TopNavigationBarEmbed.svelte"; + import EmbedHeader from "@rilldata/web-admin/features/embeds/EmbedHeader.svelte"; import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte"; import { VegaLiteTooltipHandler } from "@rilldata/web-common/components/vega/vega-tooltip.ts"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors.ts"; @@ -138,7 +138,7 @@ class="flex items-center w-full pr-4 py-1 min-h-[2.5rem]" class:border-b={!onProjectPage} > - + {/if} diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 4fab1a67bec..1679a13e62e 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -33,6 +33,7 @@ import { page } from "$app/stores"; import { V1DeploymentStatus, + type V1Organization, createAdminServiceGetCurrentUser, createAdminServiceGetDeploymentCredentials, createAdminServiceGetProject, @@ -46,12 +47,16 @@ isPublicURLPage, } from "@rilldata/web-admin/features/navigation/nav-utils"; import ProjectBuilding from "@rilldata/web-admin/features/projects/ProjectBuilding.svelte"; + import ProjectHeader from "@rilldata/web-admin/features/projects/ProjectHeader.svelte"; import ProjectTabs from "@rilldata/web-admin/features/projects/ProjectTabs.svelte"; import RedeployProjectCta from "@rilldata/web-admin/features/projects/RedeployProjectCTA.svelte"; + import SlimProjectHeader from "@rilldata/web-admin/features/projects/SlimProjectHeader.svelte"; import { createAdminServiceGetProjectWithBearerToken } from "@rilldata/web-admin/features/public-urls/get-project-with-bearer-token"; import { cloudVersion } from "@rilldata/web-admin/features/telemetry/initCloudMetrics"; + import { getThemedLogoUrl } from "@rilldata/web-admin/features/themes/organization-logo"; import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore"; import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte"; + import { themeControl } from "@rilldata/web-common/features/themes/theme-control"; import { metricsService } from "@rilldata/web-common/metrics/initMetrics"; import RuntimeProvider from "@rilldata/web-common/runtime-client/v2/RuntimeProvider.svelte"; import { RUNTIME_ACCESS_TOKEN_DEFAULT_TTL } from "@rilldata/web-common/runtime-client/constants"; @@ -67,8 +72,17 @@ $: ({ url: { pathname }, params: { organization, project, token }, + data: pageData, } = $page); + // Root layout data used by ProjectHeader / SlimProjectHeader + $: organizationPermissions = pageData?.organizationPermissions ?? {}; + $: planDisplayName = pageData?.planDisplayName; + $: organizationLogoUrl = getThemedLogoUrl( + $themeControl, + pageData?.organization as V1Organization | undefined, + ); + // Initialize view-as store for this project scope (loads from sessionStorage) $: if (organization && project) { viewAsUserStore.initForProject(organization, project); @@ -233,47 +247,70 @@ } -{#if onProjectPage && deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING} - -{/if} - -{#if error} {:else if projectData} - {#if !projectData.deployment} - - - {:else if deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING} - - {:else if deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED} - + + {#if onProjectPage && deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING} + + {/if} + + + {/key} + {:else} + - {:else if isProjectAvailable} - {#if effectiveHost != null && effectiveInstanceId} - {#key `${effectiveHost}::${effectiveInstanceId}`} - - - - {/key} + {#if !projectData.deployment} + + + {:else if deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING} + + {:else if deploymentStatus === V1DeploymentStatus.DEPLOYMENT_STATUS_ERRORED} + {:else} {/if} diff --git a/web-common/src/layout/ApplicationHeader.svelte b/web-common/src/layout/ApplicationHeader.svelte index 8250f345d6d..a764f99ecbb 100644 --- a/web-common/src/layout/ApplicationHeader.svelte +++ b/web-common/src/layout/ApplicationHeader.svelte @@ -1,6 +1,5 @@ -
+
{#if !onDeployPage} - - - + @@ -120,7 +119,7 @@ {/if} {/if} -
+
{#if mode === "Preview"} {#if route.id?.includes("explore")} @@ -135,12 +134,4 @@ {/if}
-
- - +
diff --git a/web-common/src/layout/header/Header.svelte b/web-common/src/layout/header/Header.svelte new file mode 100644 index 00000000000..6a866eda144 --- /dev/null +++ b/web-common/src/layout/header/Header.svelte @@ -0,0 +1,10 @@ + + +
+ +
diff --git a/web-common/src/layout/header/HeaderLogo.svelte b/web-common/src/layout/header/HeaderLogo.svelte new file mode 100644 index 00000000000..8a6d79fcf0e --- /dev/null +++ b/web-common/src/layout/header/HeaderLogo.svelte @@ -0,0 +1,17 @@ + + + + {#if logoUrl} + logo + {:else} + + {/if} + diff --git a/web-common/src/runtime-client/v2/RuntimeContextBridge.svelte b/web-common/src/runtime-client/v2/RuntimeContextBridge.svelte deleted file mode 100644 index d279035a882..00000000000 --- a/web-common/src/runtime-client/v2/RuntimeContextBridge.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web-common/src/runtime-client/v2/RuntimeProvider.svelte b/web-common/src/runtime-client/v2/RuntimeProvider.svelte index 0785882a1cf..1b9d1006f9f 100644 --- a/web-common/src/runtime-client/v2/RuntimeProvider.svelte +++ b/web-common/src/runtime-client/v2/RuntimeProvider.svelte @@ -7,7 +7,6 @@ getRuntimeClient, evictRuntimeClient, RUNTIME_CONTEXT_KEY, - runtimeClientStore, } from "./context"; import type { AuthContext } from "./runtime-client"; @@ -22,7 +21,6 @@ // If host/instanceId change, the parent's {#key} re-mounts us. const client = getRuntimeClient({ host, instanceId, jwt, authContext }); setContext(RUNTIME_CONTEXT_KEY, client); - runtimeClientStore.set(client); featureFlags.setRuntimeClient(client); // Handle JWT-only changes (15-min refresh, View As with same host) @@ -34,7 +32,6 @@ onDestroy(() => { featureFlags.clearRuntimeClient(); - runtimeClientStore.update((c) => (c === client ? null : c)); evictRuntimeClient(client); client.dispose(); }); diff --git a/web-common/src/runtime-client/v2/context.ts b/web-common/src/runtime-client/v2/context.ts index 5aae16fa8d6..87a05fa83c8 100644 --- a/web-common/src/runtime-client/v2/context.ts +++ b/web-common/src/runtime-client/v2/context.ts @@ -1,5 +1,4 @@ import { getContext } from "svelte"; -import { writable } from "svelte/store"; import { RuntimeClient, type AuthContext } from "./runtime-client"; export const RUNTIME_CONTEXT_KEY = Symbol("runtime-client"); @@ -54,16 +53,6 @@ export function evictRuntimeClient(client: RuntimeClient): void { } } -/** - * Module-level store that mirrors the active RuntimeClient. - * Set by RuntimeProvider on mount, cleared on destroy. - * - * Used by components that render OUTSIDE RuntimeProvider's subtree - * (e.g. TopNavigationBar in the root layout) but need reactive - * access to the current RuntimeClient. - */ -export const runtimeClientStore = writable(null); - /** * Returns the RuntimeClient set by the nearest ancestor RuntimeProvider. * Must be called during component initialization (top-level `