From 270e030ff3d5199463e691d8cc1f46cedb1452f9 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Fri, 1 Sep 2023 11:05:58 -0400 Subject: [PATCH 01/33] Refactor `UserButton` --- .../authentication/UserButton.svelte | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/web-admin/src/components/authentication/UserButton.svelte b/web-admin/src/components/authentication/UserButton.svelte index 8f9a2388c20..ec1c042425f 100644 --- a/web-admin/src/components/authentication/UserButton.svelte +++ b/web-admin/src/components/authentication/UserButton.svelte @@ -1,10 +1,14 @@ - (menuOpen = true)} + on:close={() => (menuOpen = false)} > - + + { + handleClose(); + handleLogOut(); + }}>Logout + { + handleClose(); + handleDocumentation(); + }}>Documentation + + From 0d6b7ef6868ddf65fd7ea3705dd201d60d844335 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Fri, 1 Sep 2023 11:10:46 -0400 Subject: [PATCH 02/33] Add conditional 'View as' menu item --- .../authentication/UserButton.svelte | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web-admin/src/components/authentication/UserButton.svelte b/web-admin/src/components/authentication/UserButton.svelte index ec1c042425f..ef78d4eb0b0 100644 --- a/web-admin/src/components/authentication/UserButton.svelte +++ b/web-admin/src/components/authentication/UserButton.svelte @@ -1,14 +1,20 @@ - (menuOpen = true)} - on:close={() => (menuOpen = false)} -> - avatar - + + avatar + + - {#if $page.params.organization && $page.params.project} - - { - handleClose(); - handleViewAs(); - }} + + {#if $page.params.organization && $page.params.project && $page.params.dashboard} + - View as - - - {/if} - { - handleClose(); - handleLogOut(); - }}>Logout - { - handleClose(); - handleDocumentation(); - }}>Documentation - - + + + + + View as + + + + + close1(undefined)} + /> + + + + + {/if} + { + // handleClose(); + handleLogOut(); + }}>Logout + { + // handleClose(); + handleDocumentation(); + }}>Documentation + + + diff --git a/web-admin/src/components/authentication/ViewAsUserChip.svelte b/web-admin/src/components/authentication/ViewAsUserChip.svelte new file mode 100644 index 00000000000..e61fa50e622 --- /dev/null +++ b/web-admin/src/components/authentication/ViewAsUserChip.svelte @@ -0,0 +1,60 @@ + + + + { + // updateMimickedJWT(queryClient, null); + viewAsUserStore.set(null); + }} + active={open} + > +
+ +
+
+ Viewing as {$viewAsUserStore.email} +
+
+ +
+ +
+
+
+
+
+ + close(undefined)} + /> + +
+ + Clear view + +
+
diff --git a/web-admin/src/components/authentication/ViewAsUserMenuItem.svelte b/web-admin/src/components/authentication/ViewAsUserMenuItem.svelte new file mode 100644 index 00000000000..2e2e8969d4e --- /dev/null +++ b/web-admin/src/components/authentication/ViewAsUserMenuItem.svelte @@ -0,0 +1 @@ + diff --git a/web-admin/src/components/authentication/ViewAsUserPopover.svelte b/web-admin/src/components/authentication/ViewAsUserPopover.svelte new file mode 100644 index 00000000000..a3a2725e3ea --- /dev/null +++ b/web-admin/src/components/authentication/ViewAsUserPopover.svelte @@ -0,0 +1,83 @@ + + + +
+ Test your security policies by viewing this project from the perspective of another user. +
+ + {#if visibleUsers.length > 0} +
+ {#each visibleUsers as user} + viewAsUser(user)} + > + {user.email} + + {/each} +
+ {:else} +
no results
+ {/if} +
diff --git a/web-admin/src/components/authentication/viewAsUserStore.ts b/web-admin/src/components/authentication/viewAsUserStore.ts new file mode 100644 index 00000000000..a2f9a0f0fa4 --- /dev/null +++ b/web-admin/src/components/authentication/viewAsUserStore.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; +import type { V1User } from "../../client"; + +export const viewAsUserStore = writable(null); diff --git a/web-admin/src/components/navigation/TopNavigationBar.svelte b/web-admin/src/components/navigation/TopNavigationBar.svelte index 52a365f6cea..ed3eecdee9a 100644 --- a/web-admin/src/components/navigation/TopNavigationBar.svelte +++ b/web-admin/src/components/navigation/TopNavigationBar.svelte @@ -6,6 +6,8 @@ import { createAdminServiceGetCurrentUser } from "../../client"; import SignIn from "../authentication/SignIn.svelte"; import UserButton from "../authentication/UserButton.svelte"; + import ViewAsUserChip from "../authentication/ViewAsUserChip.svelte"; + import { viewAsUserStore } from "../authentication/viewAsUserStore"; import { isErrorStoreEmpty } from "../errors/error-store"; import Breadcrumbs from "./Breadcrumbs.svelte"; @@ -38,6 +40,9 @@
{/if}
+ {#if $viewAsUserStore} + + {/if} { + runtimeState.jwt = jwt; + return runtimeState; + }); + return invalidateAllMetricsViews(queryClient, get(runtime).instanceId); +} From e136526ad87371a339137f5a03fe9b23336c50e0 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Fri, 8 Sep 2023 12:01:54 -0400 Subject: [PATCH 05/33] Acquire and set JWT --- .../[organization]/[project]/+layout.svelte | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 38d0cc8531a..b26ae8f2104 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -2,6 +2,8 @@ import { goto } from "$app/navigation"; import { page } from "$app/stores"; import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte"; + import { createAdminServiceGetDeploymentCredentials } from "../../../client"; + import { viewAsUserStore } from "../../../components/authentication/viewAsUserStore"; import { useProjectRuntime } from "../../../components/projects/selectors"; $: projRuntime = useProjectRuntime( @@ -15,13 +17,27 @@ // Redirect any nested routes (notably dashboards) to the project page goto(`/${$page.params.organization}/${$page.params.project}`); } + + // if viewAs is set (which only admins can configure), we need to update the runtime with the new jwt + $: deploymentCredsQuery = createAdminServiceGetDeploymentCredentials( + $page.params.organization, + $page.params.project, + { + userId: $viewAsUserStore?.id, + }, + { + query: { + enabled: $viewAsUserStore?.id !== undefined, + }, + } + ); {#if $projRuntime.data} From e07810f2285702cbfa9512c77652ae9ce9ce2a81 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Fri, 8 Sep 2023 20:44:02 -0400 Subject: [PATCH 06/33] Remove calls to `ListFiles` API --- .../src/components/errors/error-utils.ts | 12 +++ .../components/navigation/Breadcrumbs.svelte | 14 +-- .../components/projects/DashboardList.svelte | 46 ++++----- .../src/components/projects/dashboards.ts | 99 +++++-------------- .../[project]/[dashboard]/+page.svelte | 46 ++++----- 5 files changed, 78 insertions(+), 139 deletions(-) diff --git a/web-admin/src/components/errors/error-utils.ts b/web-admin/src/components/errors/error-utils.ts index 652ba7ab4f5..1a9a628cdb4 100644 --- a/web-admin/src/components/errors/error-utils.ts +++ b/web-admin/src/components/errors/error-utils.ts @@ -23,6 +23,18 @@ export function globalErrorCallback(error: AxiosError): void { return; } + // If on a Dashboard page, and "entry not found" (i.e. a dashboard wasn't found), + // ignore the error here, so the page can handle it. + const isDashboardPage = + get(page).route.id === "/[organization]/[project]/[dashboard]"; + if ( + isDashboardPage && + error.response.status === 400 && + (error.response.data as RpcStatus).message === "entry not found" + ) { + return; + } + // Create a pretty message for the error page const errorStoreState = createErrorStoreStateFromAxiosError(error); diff --git a/web-admin/src/components/navigation/Breadcrumbs.svelte b/web-admin/src/components/navigation/Breadcrumbs.svelte index 68fd1aa19ba..362acac495f 100644 --- a/web-admin/src/components/navigation/Breadcrumbs.svelte +++ b/web-admin/src/components/navigation/Breadcrumbs.svelte @@ -11,7 +11,7 @@ createAdminServiceListOrganizations, createAdminServiceListProjectsForOrganization, } from "../../client"; - import { useDashboardListItems } from "../projects/dashboards"; + import { useDashboards } from "../projects/dashboards"; import BreadcrumbItem from "./BreadcrumbItem.svelte"; import OrganizationAvatar from "./OrganizationAvatar.svelte"; @@ -43,8 +43,8 @@ ); $: isProjectPage = $page.route.id === "/[organization]/[project]"; - $: dashboardListItems = useDashboardListItems(instanceId); - $: currentDashboard = $dashboardListItems?.items?.find( + $: dashboards = useDashboards(instanceId); + $: currentDashboard = $dashboards?.data?.find( (listing) => listing.name === $page.params.dashboard ); $: isDashboardPage = @@ -92,13 +92,13 @@ {#if currentDashboard} / 1 && - $dashboardListItems.items.map((listing) => { + menuOptions={$dashboards?.data?.length > 1 && + $dashboards.data.map((listing) => { return { key: listing.name, - main: listing?.title || listing.name, + main: listing?.label || listing.name, }; })} menuKey={currentDashboard.name} diff --git a/web-admin/src/components/projects/DashboardList.svelte b/web-admin/src/components/projects/DashboardList.svelte index a2aa69daa0e..d7ad9c82d20 100644 --- a/web-admin/src/components/projects/DashboardList.svelte +++ b/web-admin/src/components/projects/DashboardList.svelte @@ -1,23 +1,17 @@ -{#if dashboardListItems?.length === 0} +{#if dashboards?.length === 0}

This project has no dashboards yet.

-{:else if dashboardListItems?.length > 0} +{:else if dashboards?.length > 0}
    - {#each dashboardListItems as dashboardListItem} + {#each dashboards as dashboard}
  1. -
    - {dashboardListItem?.title || dashboardListItem.name} + {dashboard?.label || dashboard.name} + - {#if $proj.data.prodDeployment.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_RECONCILING && !dashboardListItem.isValid} + - {#if dashboardListItem.description} + {#if dashboard.description} {dashboard.description} {/if}
    -
    +
  2. {/each}
diff --git a/web-admin/src/components/projects/dashboards.ts b/web-admin/src/components/projects/dashboards.ts index c0de4c64003..d07c9b9203d 100644 --- a/web-admin/src/components/projects/dashboards.ts +++ b/web-admin/src/components/projects/dashboards.ts @@ -1,11 +1,10 @@ import type { V1GetProjectResponse } from "@rilldata/web-admin/client"; -import type { V1CatalogEntry } from "@rilldata/web-common/runtime-client"; -import { - createRuntimeServiceListCatalogEntries, - createRuntimeServiceListFiles, +import type { + V1CatalogEntry, + V1MetricsView, } from "@rilldata/web-common/runtime-client"; +import { createRuntimeServiceListCatalogEntries } from "@rilldata/web-common/runtime-client"; import Axios from "axios"; -import { Readable, derived } from "svelte/store"; export interface DashboardListItem { name: string; @@ -16,7 +15,7 @@ export interface DashboardListItem { export async function getDashboardsForProject( projectData: V1GetProjectResponse -): Promise { +): Promise { // There may not be a prodDeployment if the project was hibernated if (!projectData.prodDeployment) { return []; @@ -35,84 +34,32 @@ export async function getDashboardsForProject( }, }); - // get all valid and invalid dashboards - const filesRequest = axios.get( - `/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/files?glob=dashboards/*.yaml` - ); - - // get the valid dashboards - const catalogEntriesRequest = axios.get( + const catalogEntriesResponse = await axios.get( `/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/catalog?type=OBJECT_TYPE_METRICS_VIEW` ); - const [filesResponse, catalogEntriesResponse] = await Promise.all([ - filesRequest, - catalogEntriesRequest, - ]); - - const filePaths = filesResponse.data?.paths; - const catalogEntries = catalogEntriesResponse.data?.entries; + const catalogEntries = catalogEntriesResponse.data + ?.entries as V1CatalogEntry[]; - // compose the dashboard list items - const dashboardListItems = getDashboardListItemsFromFilesAndCatalogEntries( - filePaths, - catalogEntries + const dashboards = catalogEntries?.map( + (entry: V1CatalogEntry) => entry.metricsView ); - return dashboardListItems; + return dashboards; } -export function getDashboardListItemsFromFilesAndCatalogEntries( - filePaths: string[], - catalogEntries: V1CatalogEntry[] -): DashboardListItem[] { - const dashboardListings = filePaths?.map((path: string) => { - const name = path.replace("/dashboards/", "").replace(".yaml", ""); - const catalogEntry = catalogEntries?.find( - (entry: V1CatalogEntry) => entry.path === path - ); - const title = catalogEntry?.metricsView?.label; - const description = catalogEntry?.metricsView?.description; - // invalid dashboards are not in the catalog - const isValid = !!catalogEntry; - return { - name, - title, - description, - isValid, - }; - }); - - return dashboardListings; -} - -export function useDashboardListItems(instanceId: string): Readable<{ - items: DashboardListItem[]; - isSuccess: boolean; -}> { - return derived( - [ - createRuntimeServiceListFiles(instanceId, { - glob: "dashboards/*.yaml", - }), - createRuntimeServiceListCatalogEntries(instanceId, { - type: "OBJECT_TYPE_METRICS_VIEW", - }), - ], - ([dashboardFiles, dashboardCatalogEntries]) => { - if (!dashboardFiles.isSuccess || !dashboardCatalogEntries.isSuccess) - return { - isSuccess: false, - items: [], - }; - - return { - isSuccess: true, - items: getDashboardListItemsFromFilesAndCatalogEntries( - dashboardFiles?.data?.paths ?? [], - dashboardCatalogEntries?.data?.entries ?? [] - ), - }; +export function useDashboards(instanceId: string) { + return createRuntimeServiceListCatalogEntries( + instanceId, + { + type: "OBJECT_TYPE_METRICS_VIEW", + }, + { + query: { + select: (data) => { + return data.entries.map((entry) => entry.metricsView); + }, + }, } ); } diff --git a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte index c858bb44c09..b6e3ce8938c 100644 --- a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte @@ -4,10 +4,7 @@ createAdminServiceGetProject, V1DeploymentStatus, } from "@rilldata/web-admin/client"; - import { - getDashboardsForProject, - useDashboardListItems, - } from "@rilldata/web-admin/components/projects/dashboards"; + import { getDashboardsForProject } from "@rilldata/web-admin/components/projects/dashboards"; import { invalidateDashboardsQueries } from "@rilldata/web-admin/components/projects/invalidations"; import { useProjectDeploymentStatus } from "@rilldata/web-admin/components/projects/selectors"; import { Dashboard } from "@rilldata/web-common/features/dashboards"; @@ -15,14 +12,13 @@ import DashboardURLStateProvider from "@rilldata/web-common/features/dashboards/proto-state/DashboardURLStateProvider.svelte"; import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte"; import { - getRuntimeServiceListCatalogEntriesQueryKey, - getRuntimeServiceListFilesQueryKey, + createRuntimeServiceGetCatalogEntry, + getRuntimeServiceGetCatalogEntryQueryKey, } from "@rilldata/web-common/runtime-client"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import { useQueryClient } from "@tanstack/svelte-query"; import { errorStore } from "../../../../components/errors/error-store"; import ProjectBuilding from "../../../../components/projects/ProjectBuilding.svelte"; - import ProjectErrored from "../../../../components/projects/ProjectErrored.svelte"; const queryClient = useQueryClient(); @@ -44,6 +40,8 @@ $: isProjectErrored = $projectDeploymentStatus.data === V1DeploymentStatus.DEPLOYMENT_STATUS_ERROR; + $: isProjectUpdating = isProjectPending || isProjectReconciling; + $: isProjectStatic = isProjectOK || isProjectErrored; let isProjectOK: boolean; @@ -56,16 +54,9 @@ if (projectWasNotOk && isProjectOK) { getDashboardsAndInvalidate(); - // Invalidate the queries used to assess dashboard validity - queryClient.invalidateQueries( - getRuntimeServiceListFilesQueryKey(instanceId, { - glob: "dashboards/*.yaml", - }) - ); + // Invalidate the query used to assess dashboard validity queryClient.invalidateQueries( - getRuntimeServiceListCatalogEntriesQueryKey(instanceId, { - type: "OBJECT_TYPE_METRICS_VIEW", - }) + getRuntimeServiceGetCatalogEntryQueryKey(instanceId, dashboardName) ); } } @@ -76,17 +67,14 @@ return invalidateDashboardsQueries(queryClient, dashboardNames); } - // We avoid calling `GetCatalogEntry` to check for dashboard validity because that would trigger a 404 page. - $: dashboardListItems = useDashboardListItems(instanceId); - $: currentDashboard = $dashboardListItems?.items?.find( - (listing) => listing.name === $page.params.dashboard - ); - $: isDashboardOK = currentDashboard?.isValid; - $: isDashboardErrored = !!currentDashboard && !currentDashboard.isValid; - $: isDashboardNotFound = $dashboardListItems?.isSuccess && !currentDashboard; + $: dashboard = createRuntimeServiceGetCatalogEntry(instanceId, dashboardName); + $: isDashboardOK = $dashboard.isSuccess; + $: isDashboardNotFound = + $dashboard.isError && $dashboard.error?.response?.status === 400; + // isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile // If no dashboard is found, show a 404 page - if ((isProjectOK || isProjectErrored) && isDashboardNotFound) { + $: if (isProjectStatic && isDashboardNotFound) { errorStore.set({ statusCode: 404, header: "Dashboard not found", @@ -102,7 +90,7 @@ -{#if isProjectPending || (isProjectReconciling && isDashboardNotFound)} +{#if isProjectUpdating && isDashboardNotFound} {:else if isDashboardOK} @@ -114,6 +102,8 @@ {/key} -{:else if isDashboardErrored} - {/if} + + From d5c1d2617351f44e3cd7807b42c917aeb3fa36fb Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 09:01:54 -0400 Subject: [PATCH 07/33] Fix `svelte-check` w/ explicit type --- .../[organization]/[project]/[dashboard]/+page.svelte | 4 +++- web-common/src/runtime-client/error.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 web-common/src/runtime-client/error.ts diff --git a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte index b6e3ce8938c..d074b7c1e59 100644 --- a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte @@ -15,6 +15,7 @@ createRuntimeServiceGetCatalogEntry, getRuntimeServiceGetCatalogEntryQueryKey, } from "@rilldata/web-common/runtime-client"; + import type { QueryError } from "@rilldata/web-common/runtime-client/error"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import { useQueryClient } from "@tanstack/svelte-query"; import { errorStore } from "../../../../components/errors/error-store"; @@ -70,7 +71,8 @@ $: dashboard = createRuntimeServiceGetCatalogEntry(instanceId, dashboardName); $: isDashboardOK = $dashboard.isSuccess; $: isDashboardNotFound = - $dashboard.isError && $dashboard.error?.response?.status === 400; + $dashboard.isError && + ($dashboard.error as QueryError)?.response?.status === 400; // isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile // If no dashboard is found, show a 404 page diff --git a/web-common/src/runtime-client/error.ts b/web-common/src/runtime-client/error.ts new file mode 100644 index 00000000000..6578d514868 --- /dev/null +++ b/web-common/src/runtime-client/error.ts @@ -0,0 +1,9 @@ +export interface QueryError { + response: { + status: number; + data: { + message: string; + }; + }; + message: string; +} From d37bd4fff1692e1ee19b195e50f3a841127184b9 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 09:04:20 -0400 Subject: [PATCH 08/33] Add comment --- web-common/src/runtime-client/error.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web-common/src/runtime-client/error.ts b/web-common/src/runtime-client/error.ts index 6578d514868..d40388f2ba2 100644 --- a/web-common/src/runtime-client/error.ts +++ b/web-common/src/runtime-client/error.ts @@ -1,3 +1,4 @@ +// The Orval-generated type for query errors is `RpcStatus`, but the following is the observed type that is actually returned. export interface QueryError { response: { status: number; From e282ec4e11d87bac3d9b5e7263e3ccac2478a74b Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 09:14:52 -0400 Subject: [PATCH 09/33] Patch `MenuItem` component --- .../src/components/menu/core/MenuItem.svelte | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/web-common/src/components/menu/core/MenuItem.svelte b/web-common/src/components/menu/core/MenuItem.svelte index 17b3f8a2a7e..3a354a5baa8 100644 --- a/web-common/src/components/menu/core/MenuItem.svelte +++ b/web-common/src/components/menu/core/MenuItem.svelte @@ -71,6 +71,12 @@ } let hovered = false; + function onMouseOver() { + if (!disabled) { + hovered = true; + } + } + function onFocus() { if (!disabled) { $currentItem = itemID; @@ -124,6 +130,7 @@ ? 'rgb(75, 85, 99)' : 'rgb(235, 235, 235)'}" class=" + w-full text-left py-1 {icon ? 'px-2' : 'px-3'} @@ -146,7 +153,7 @@ class:selected class:cursor-not-allowed={disabled} aria-disabled={disabled} - on:mouseover={onFocus} + on:mouseover={onMouseOver} on:mouseleave={onBlur} on:focus={onFocus} on:blur={() => { @@ -154,7 +161,7 @@ hovered = false; } }} - on:click|stopPropagation={handleClick} + on:click={handleClick} > {#if icon}
{#if $$slots["right"]} -
+
{/if} From 37e03d7e2f5b1fcb98e95171d2e75753ed98cb6b Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 09:17:13 -0400 Subject: [PATCH 10/33] Remove File API usage --- .../src/components/errors/error-utils.ts | 12 +++ .../components/navigation/Breadcrumbs.svelte | 14 +-- .../components/projects/DashboardList.svelte | 46 ++++----- .../src/components/projects/dashboards.ts | 99 +++++-------------- .../[project]/[dashboard]/+page.svelte | 48 ++++----- web-common/src/runtime-client/error.ts | 10 ++ 6 files changed, 90 insertions(+), 139 deletions(-) create mode 100644 web-common/src/runtime-client/error.ts diff --git a/web-admin/src/components/errors/error-utils.ts b/web-admin/src/components/errors/error-utils.ts index 652ba7ab4f5..1a9a628cdb4 100644 --- a/web-admin/src/components/errors/error-utils.ts +++ b/web-admin/src/components/errors/error-utils.ts @@ -23,6 +23,18 @@ export function globalErrorCallback(error: AxiosError): void { return; } + // If on a Dashboard page, and "entry not found" (i.e. a dashboard wasn't found), + // ignore the error here, so the page can handle it. + const isDashboardPage = + get(page).route.id === "/[organization]/[project]/[dashboard]"; + if ( + isDashboardPage && + error.response.status === 400 && + (error.response.data as RpcStatus).message === "entry not found" + ) { + return; + } + // Create a pretty message for the error page const errorStoreState = createErrorStoreStateFromAxiosError(error); diff --git a/web-admin/src/components/navigation/Breadcrumbs.svelte b/web-admin/src/components/navigation/Breadcrumbs.svelte index 68fd1aa19ba..362acac495f 100644 --- a/web-admin/src/components/navigation/Breadcrumbs.svelte +++ b/web-admin/src/components/navigation/Breadcrumbs.svelte @@ -11,7 +11,7 @@ createAdminServiceListOrganizations, createAdminServiceListProjectsForOrganization, } from "../../client"; - import { useDashboardListItems } from "../projects/dashboards"; + import { useDashboards } from "../projects/dashboards"; import BreadcrumbItem from "./BreadcrumbItem.svelte"; import OrganizationAvatar from "./OrganizationAvatar.svelte"; @@ -43,8 +43,8 @@ ); $: isProjectPage = $page.route.id === "/[organization]/[project]"; - $: dashboardListItems = useDashboardListItems(instanceId); - $: currentDashboard = $dashboardListItems?.items?.find( + $: dashboards = useDashboards(instanceId); + $: currentDashboard = $dashboards?.data?.find( (listing) => listing.name === $page.params.dashboard ); $: isDashboardPage = @@ -92,13 +92,13 @@ {#if currentDashboard} / 1 && - $dashboardListItems.items.map((listing) => { + menuOptions={$dashboards?.data?.length > 1 && + $dashboards.data.map((listing) => { return { key: listing.name, - main: listing?.title || listing.name, + main: listing?.label || listing.name, }; })} menuKey={currentDashboard.name} diff --git a/web-admin/src/components/projects/DashboardList.svelte b/web-admin/src/components/projects/DashboardList.svelte index a2aa69daa0e..d7ad9c82d20 100644 --- a/web-admin/src/components/projects/DashboardList.svelte +++ b/web-admin/src/components/projects/DashboardList.svelte @@ -1,23 +1,17 @@ -{#if dashboardListItems?.length === 0} +{#if dashboards?.length === 0}

This project has no dashboards yet.

-{:else if dashboardListItems?.length > 0} +{:else if dashboards?.length > 0}
    - {#each dashboardListItems as dashboardListItem} + {#each dashboards as dashboard}
  1. -
    - {dashboardListItem?.title || dashboardListItem.name} + {dashboard?.label || dashboard.name} + - {#if $proj.data.prodDeployment.status !== V1DeploymentStatus.DEPLOYMENT_STATUS_RECONCILING && !dashboardListItem.isValid} + - {#if dashboardListItem.description} + {#if dashboard.description} {dashboard.description} {/if}
    -
    +
  2. {/each}
diff --git a/web-admin/src/components/projects/dashboards.ts b/web-admin/src/components/projects/dashboards.ts index c0de4c64003..d07c9b9203d 100644 --- a/web-admin/src/components/projects/dashboards.ts +++ b/web-admin/src/components/projects/dashboards.ts @@ -1,11 +1,10 @@ import type { V1GetProjectResponse } from "@rilldata/web-admin/client"; -import type { V1CatalogEntry } from "@rilldata/web-common/runtime-client"; -import { - createRuntimeServiceListCatalogEntries, - createRuntimeServiceListFiles, +import type { + V1CatalogEntry, + V1MetricsView, } from "@rilldata/web-common/runtime-client"; +import { createRuntimeServiceListCatalogEntries } from "@rilldata/web-common/runtime-client"; import Axios from "axios"; -import { Readable, derived } from "svelte/store"; export interface DashboardListItem { name: string; @@ -16,7 +15,7 @@ export interface DashboardListItem { export async function getDashboardsForProject( projectData: V1GetProjectResponse -): Promise { +): Promise { // There may not be a prodDeployment if the project was hibernated if (!projectData.prodDeployment) { return []; @@ -35,84 +34,32 @@ export async function getDashboardsForProject( }, }); - // get all valid and invalid dashboards - const filesRequest = axios.get( - `/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/files?glob=dashboards/*.yaml` - ); - - // get the valid dashboards - const catalogEntriesRequest = axios.get( + const catalogEntriesResponse = await axios.get( `/v1/instances/${projectData.prodDeployment.runtimeInstanceId}/catalog?type=OBJECT_TYPE_METRICS_VIEW` ); - const [filesResponse, catalogEntriesResponse] = await Promise.all([ - filesRequest, - catalogEntriesRequest, - ]); - - const filePaths = filesResponse.data?.paths; - const catalogEntries = catalogEntriesResponse.data?.entries; + const catalogEntries = catalogEntriesResponse.data + ?.entries as V1CatalogEntry[]; - // compose the dashboard list items - const dashboardListItems = getDashboardListItemsFromFilesAndCatalogEntries( - filePaths, - catalogEntries + const dashboards = catalogEntries?.map( + (entry: V1CatalogEntry) => entry.metricsView ); - return dashboardListItems; + return dashboards; } -export function getDashboardListItemsFromFilesAndCatalogEntries( - filePaths: string[], - catalogEntries: V1CatalogEntry[] -): DashboardListItem[] { - const dashboardListings = filePaths?.map((path: string) => { - const name = path.replace("/dashboards/", "").replace(".yaml", ""); - const catalogEntry = catalogEntries?.find( - (entry: V1CatalogEntry) => entry.path === path - ); - const title = catalogEntry?.metricsView?.label; - const description = catalogEntry?.metricsView?.description; - // invalid dashboards are not in the catalog - const isValid = !!catalogEntry; - return { - name, - title, - description, - isValid, - }; - }); - - return dashboardListings; -} - -export function useDashboardListItems(instanceId: string): Readable<{ - items: DashboardListItem[]; - isSuccess: boolean; -}> { - return derived( - [ - createRuntimeServiceListFiles(instanceId, { - glob: "dashboards/*.yaml", - }), - createRuntimeServiceListCatalogEntries(instanceId, { - type: "OBJECT_TYPE_METRICS_VIEW", - }), - ], - ([dashboardFiles, dashboardCatalogEntries]) => { - if (!dashboardFiles.isSuccess || !dashboardCatalogEntries.isSuccess) - return { - isSuccess: false, - items: [], - }; - - return { - isSuccess: true, - items: getDashboardListItemsFromFilesAndCatalogEntries( - dashboardFiles?.data?.paths ?? [], - dashboardCatalogEntries?.data?.entries ?? [] - ), - }; +export function useDashboards(instanceId: string) { + return createRuntimeServiceListCatalogEntries( + instanceId, + { + type: "OBJECT_TYPE_METRICS_VIEW", + }, + { + query: { + select: (data) => { + return data.entries.map((entry) => entry.metricsView); + }, + }, } ); } diff --git a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte index c858bb44c09..d074b7c1e59 100644 --- a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte @@ -4,10 +4,7 @@ createAdminServiceGetProject, V1DeploymentStatus, } from "@rilldata/web-admin/client"; - import { - getDashboardsForProject, - useDashboardListItems, - } from "@rilldata/web-admin/components/projects/dashboards"; + import { getDashboardsForProject } from "@rilldata/web-admin/components/projects/dashboards"; import { invalidateDashboardsQueries } from "@rilldata/web-admin/components/projects/invalidations"; import { useProjectDeploymentStatus } from "@rilldata/web-admin/components/projects/selectors"; import { Dashboard } from "@rilldata/web-common/features/dashboards"; @@ -15,14 +12,14 @@ import DashboardURLStateProvider from "@rilldata/web-common/features/dashboards/proto-state/DashboardURLStateProvider.svelte"; import StateManagersProvider from "@rilldata/web-common/features/dashboards/state-managers/StateManagersProvider.svelte"; import { - getRuntimeServiceListCatalogEntriesQueryKey, - getRuntimeServiceListFilesQueryKey, + createRuntimeServiceGetCatalogEntry, + getRuntimeServiceGetCatalogEntryQueryKey, } from "@rilldata/web-common/runtime-client"; + import type { QueryError } from "@rilldata/web-common/runtime-client/error"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import { useQueryClient } from "@tanstack/svelte-query"; import { errorStore } from "../../../../components/errors/error-store"; import ProjectBuilding from "../../../../components/projects/ProjectBuilding.svelte"; - import ProjectErrored from "../../../../components/projects/ProjectErrored.svelte"; const queryClient = useQueryClient(); @@ -44,6 +41,8 @@ $: isProjectErrored = $projectDeploymentStatus.data === V1DeploymentStatus.DEPLOYMENT_STATUS_ERROR; + $: isProjectUpdating = isProjectPending || isProjectReconciling; + $: isProjectStatic = isProjectOK || isProjectErrored; let isProjectOK: boolean; @@ -56,16 +55,9 @@ if (projectWasNotOk && isProjectOK) { getDashboardsAndInvalidate(); - // Invalidate the queries used to assess dashboard validity - queryClient.invalidateQueries( - getRuntimeServiceListFilesQueryKey(instanceId, { - glob: "dashboards/*.yaml", - }) - ); + // Invalidate the query used to assess dashboard validity queryClient.invalidateQueries( - getRuntimeServiceListCatalogEntriesQueryKey(instanceId, { - type: "OBJECT_TYPE_METRICS_VIEW", - }) + getRuntimeServiceGetCatalogEntryQueryKey(instanceId, dashboardName) ); } } @@ -76,17 +68,15 @@ return invalidateDashboardsQueries(queryClient, dashboardNames); } - // We avoid calling `GetCatalogEntry` to check for dashboard validity because that would trigger a 404 page. - $: dashboardListItems = useDashboardListItems(instanceId); - $: currentDashboard = $dashboardListItems?.items?.find( - (listing) => listing.name === $page.params.dashboard - ); - $: isDashboardOK = currentDashboard?.isValid; - $: isDashboardErrored = !!currentDashboard && !currentDashboard.isValid; - $: isDashboardNotFound = $dashboardListItems?.isSuccess && !currentDashboard; + $: dashboard = createRuntimeServiceGetCatalogEntry(instanceId, dashboardName); + $: isDashboardOK = $dashboard.isSuccess; + $: isDashboardNotFound = + $dashboard.isError && + ($dashboard.error as QueryError)?.response?.status === 400; + // isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile // If no dashboard is found, show a 404 page - if ((isProjectOK || isProjectErrored) && isDashboardNotFound) { + $: if (isProjectStatic && isDashboardNotFound) { errorStore.set({ statusCode: 404, header: "Dashboard not found", @@ -102,7 +92,7 @@ -{#if isProjectPending || (isProjectReconciling && isDashboardNotFound)} +{#if isProjectUpdating && isDashboardNotFound} {:else if isDashboardOK} @@ -114,6 +104,8 @@ {/key} -{:else if isDashboardErrored} - {/if} + + diff --git a/web-common/src/runtime-client/error.ts b/web-common/src/runtime-client/error.ts new file mode 100644 index 00000000000..d40388f2ba2 --- /dev/null +++ b/web-common/src/runtime-client/error.ts @@ -0,0 +1,10 @@ +// The Orval-generated type for query errors is `RpcStatus`, but the following is the observed type that is actually returned. +export interface QueryError { + response: { + status: number; + data: { + message: string; + }; + }; + message: string; +} From d74f876024a8a0afb06671d19ba77ca00cb1de03 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 22:22:27 -0400 Subject: [PATCH 11/33] Handle chip removal --- .../components/authentication/ViewAsUserChip.svelte | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/web-admin/src/components/authentication/ViewAsUserChip.svelte b/web-admin/src/components/authentication/ViewAsUserChip.svelte index e61fa50e622..6277be52c71 100644 --- a/web-admin/src/components/authentication/ViewAsUserChip.svelte +++ b/web-admin/src/components/authentication/ViewAsUserChip.svelte @@ -8,7 +8,10 @@ import { IconSpaceFixer } from "@rilldata/web-common/components/button"; import { Chip } from "@rilldata/web-common/components/chip"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; + import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; + import { useQueryClient } from "@tanstack/svelte-query"; import { createPopperActions } from "svelte-popperjs"; + import { errorStore } from "../errors/error-store"; import ViewAsUserPopover from "./ViewAsUserPopover.svelte"; import { viewAsUserStore } from "./viewAsUserStore"; @@ -19,14 +22,18 @@ strategy: "fixed", modifiers: [{ name: "offset", options: { offset: [0, 4] } }], }; + + const queryClient = useQueryClient(); + $: org = $page.params.organization; + $: project = $page.params.project; { - // updateMimickedJWT(queryClient, null); - viewAsUserStore.set(null); + on:remove={async () => { + await updateMimickedJWT(queryClient, org, project, null); + errorStore.reset(); }} active={open} > From c0b90512ebaad0817675d5c8ea8bec13895295ab Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 22:22:56 -0400 Subject: [PATCH 12/33] Fix z-index on chip --- web-admin/src/components/authentication/ViewAsUserChip.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-admin/src/components/authentication/ViewAsUserChip.svelte b/web-admin/src/components/authentication/ViewAsUserChip.svelte index 6277be52c71..2f811aa7111 100644 --- a/web-admin/src/components/authentication/ViewAsUserChip.svelte +++ b/web-admin/src/components/authentication/ViewAsUserChip.svelte @@ -52,7 +52,7 @@
- + Date: Mon, 11 Sep 2023 22:25:08 -0400 Subject: [PATCH 13/33] Clear error when switching users --- .../src/components/authentication/ViewAsUserPopover.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web-admin/src/components/authentication/ViewAsUserPopover.svelte b/web-admin/src/components/authentication/ViewAsUserPopover.svelte index a3a2725e3ea..e4e59ec7807 100644 --- a/web-admin/src/components/authentication/ViewAsUserPopover.svelte +++ b/web-admin/src/components/authentication/ViewAsUserPopover.svelte @@ -6,6 +6,7 @@ import { matchSorter } from "match-sorter"; import { createEventDispatcher } from "svelte"; import { createAdminServiceSearchProjectUsers, V1User } from "../../client"; + import { errorStore } from "../errors/error-store"; import { viewAsUserStore } from "./viewAsUserStore"; export let organization: string; @@ -21,9 +22,10 @@ const dispatch = createEventDispatcher(); const queryClient = useQueryClient(); - function viewAsUser(user: V1User) { + async function viewAsUser(user: V1User) { viewAsUserStore.set(user); - updateMimickedJWT(queryClient, organization, project, user); + await updateMimickedJWT(queryClient, organization, project, user); + errorStore.reset(); dispatch("select"); } From 1b66017b8e403fee869d70bd8ba113fe7043470e Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 22:26:01 -0400 Subject: [PATCH 14/33] Switch back to admin's JWT --- .../granular-access-policies/updateMimickedJWT.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts b/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts index 5b70aa9c83b..51cabe1c106 100644 --- a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts +++ b/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts @@ -1,5 +1,8 @@ import { adminServiceGetDeploymentCredentials, + adminServiceGetProject, + getAdminServiceGetProjectQueryKey, + V1GetProjectResponse, type V1User, } from "@rilldata/web-admin/client"; import { viewAsUserStore } from "@rilldata/web-admin/components/authentication/viewAsUserStore"; @@ -30,6 +33,13 @@ export async function updateMimickedJWT( } catch (e) { // no-op } + } else { + // Get the admin's JWT from `GetProject` call + const projResp = await queryClient.fetchQuery({ + queryKey: getAdminServiceGetProjectQueryKey(organization, project), + queryFn: () => adminServiceGetProject(organization, project), + }); + jwt = projResp.jwt; } // selectedMockUserJWT.set(jwt); From adda4993527c48f08d8022e8b7e12cc7af939268 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 11 Sep 2023 22:26:52 -0400 Subject: [PATCH 15/33] Handle 404 from `GetCatalog` --- web-admin/src/components/errors/error-utils.ts | 4 ++-- .../routes/[organization]/[project]/[dashboard]/+page.svelte | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web-admin/src/components/errors/error-utils.ts b/web-admin/src/components/errors/error-utils.ts index 1a9a628cdb4..6311b9776b8 100644 --- a/web-admin/src/components/errors/error-utils.ts +++ b/web-admin/src/components/errors/error-utils.ts @@ -29,8 +29,8 @@ export function globalErrorCallback(error: AxiosError): void { get(page).route.id === "/[organization]/[project]/[dashboard]"; if ( isDashboardPage && - error.response.status === 400 && - (error.response.data as RpcStatus).message === "entry not found" + error.response.status === 404 && + (error.response.data as RpcStatus).message === "not found" ) { return; } diff --git a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte index d074b7c1e59..0f557b79e65 100644 --- a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte @@ -72,7 +72,7 @@ $: isDashboardOK = $dashboard.isSuccess; $: isDashboardNotFound = $dashboard.isError && - ($dashboard.error as QueryError)?.response?.status === 400; + ($dashboard.error as QueryError)?.response?.status === 404; // isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile // If no dashboard is found, show a 404 page From 742e66305b98085be4e58189c88e9c9a686e8fea Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 17:26:08 -0400 Subject: [PATCH 16/33] Add workaround for metrics view 401s; clean up --- .../src/components/errors/error-utils.ts | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/web-admin/src/components/errors/error-utils.ts b/web-admin/src/components/errors/error-utils.ts index 6311b9776b8..b5f1160aae3 100644 --- a/web-admin/src/components/errors/error-utils.ts +++ b/web-admin/src/components/errors/error-utils.ts @@ -7,31 +7,42 @@ import { ADMIN_URL } from "../../client/http-client"; import { ErrorStoreState, errorStore } from "./error-store"; export function globalErrorCallback(error: AxiosError): void { - // If Unauthorized, redirect to login page - if (error.response.status === 401) { - goto(`${ADMIN_URL}/auth/login?redirect=${window.origin}`); - return; + const isProjectPage = get(page).route.id === "/[organization]/[project]"; + const isDashboardPage = + get(page).route.id === "/[organization]/[project]/[dashboard]"; + + // Special handling for some errors on the Project page + if (isProjectPage) { + // If "repository not found", ignore the error and show the page + if ( + error.response.status === 400 && + (error.response.data as RpcStatus).message === "repository not found" + ) { + return; + } } - // If on a Project page, and "repository not found", ignore the error and show the page - const isProjectPage = get(page).route.id === "/[organization]/[project]"; - if ( - isProjectPage && - error.response.status === 400 && - (error.response.data as RpcStatus).message === "repository not found" - ) { - return; + // Special handling for some errors on the Dashboard page + if (isDashboardPage) { + // If a dashboard wasn't found, let +page.svelte handle the error. + // Because the project may be reconciling, in which case we should show a loading spinner not a 404. + if ( + error.response.status === 404 && + (error.response.data as RpcStatus).message === "not found" + ) { + return; + } + + // When a JWT doesn't permit access to a metrics view, the metrics view APIs return 401s. + // In this scenario, `GetCatalog` returns a 404. We ignore the 401s so we can show the 404. + if (error.response.status === 401) { + return; + } } - // If on a Dashboard page, and "entry not found" (i.e. a dashboard wasn't found), - // ignore the error here, so the page can handle it. - const isDashboardPage = - get(page).route.id === "/[organization]/[project]/[dashboard]"; - if ( - isDashboardPage && - error.response.status === 404 && - (error.response.data as RpcStatus).message === "not found" - ) { + // If Unauthorized, redirect to login page + if (error.response.status === 401) { + goto(`${ADMIN_URL}/auth/login?redirect=${window.origin}`); return; } From 8ac38ef52dea623fa80525339dd010a6db2bf90a Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 18:22:10 -0400 Subject: [PATCH 17/33] Clear mimicked user after navigation --- .../clearMimickedUserAfterNavigate.ts | 37 +++++++++++++++++++ web-admin/src/routes/+layout.svelte | 2 + 2 files changed, 39 insertions(+) create mode 100644 web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts diff --git a/web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts b/web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts new file mode 100644 index 00000000000..cfb0ae02024 --- /dev/null +++ b/web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts @@ -0,0 +1,37 @@ +import { afterNavigate } from "$app/navigation"; +import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; +import { invalidateRuntimeQueries } from "@rilldata/web-common/runtime-client/invalidation"; +import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; +import type { QueryClient } from "@tanstack/svelte-query"; +import { get } from "svelte/store"; +import { viewAsUserStore } from "./viewAsUserStore"; + +/** + * Remove the mimicked user (if any) when navigating away from the Dashboard page + */ +export function clearMimickedUserAfterNavigate(queryClient: QueryClient) { + afterNavigate((nav) => { + // Only applies if mimicking a user on the Dashboard page + if (!get(viewAsUserStore) || !nav.from?.params?.dashboard) return; + + // If remaining within the project, set the admin's JWT + if (!nav.to.params.dashboard && nav.to.params.project) { + updateMimickedJWT( + queryClient, + nav.to.params.organization, + nav.to.params.project, + null + ); + } + + // If leaving a project, clear the JWT + if (!nav.to.params.dashboard && !nav.to.params.project) { + viewAsUserStore.set(null); + runtime.update((runtimeState) => { + runtimeState.jwt = null; + return runtimeState; + }); + invalidateRuntimeQueries(queryClient); + } + }); +} diff --git a/web-admin/src/routes/+layout.svelte b/web-admin/src/routes/+layout.svelte index 8f9c156b663..14e0e82b123 100644 --- a/web-admin/src/routes/+layout.svelte +++ b/web-admin/src/routes/+layout.svelte @@ -11,6 +11,7 @@ QueryClient, QueryClientProvider, } from "@tanstack/svelte-query"; + import { clearMimickedUserAfterNavigate } from "../components/authentication/clearMimickedUserAfterNavigate"; import { globalErrorCallback } from "../components/errors/error-utils"; import ErrorBoundary from "../components/errors/ErrorBoundary.svelte"; import TopNavigationBar from "../components/navigation/TopNavigationBar.svelte"; @@ -38,6 +39,7 @@ }); beforeNavigate(retainFeaturesFlags); + clearMimickedUserAfterNavigate(queryClient); From 1063b9c7898e3b3c230d18e09f2c27869657e396 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 18:22:27 -0400 Subject: [PATCH 18/33] Bugfix --- web-common/src/runtime-client/invalidation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-common/src/runtime-client/invalidation.ts b/web-common/src/runtime-client/invalidation.ts index 9d5afd17606..5060ba4056d 100644 --- a/web-common/src/runtime-client/invalidation.ts +++ b/web-common/src/runtime-client/invalidation.ts @@ -19,7 +19,7 @@ import { get } from "svelte/store"; // invalidation helpers export function invalidateRuntimeQueries(queryClient: QueryClient) { - return queryClient.invalidateQueries({ + return queryClient.resetQueries({ predicate: (query) => typeof query.queryKey[0] === "string" && query.queryKey[0].startsWith("/v1/instances"), From 30568014c59286c49460983ad899864a5967416b Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 18:35:35 -0400 Subject: [PATCH 19/33] Make the whole chip clickable --- .../authentication/ViewAsUserChip.svelte | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/web-admin/src/components/authentication/ViewAsUserChip.svelte b/web-admin/src/components/authentication/ViewAsUserChip.svelte index 2f811aa7111..e34932f586b 100644 --- a/web-admin/src/components/authentication/ViewAsUserChip.svelte +++ b/web-admin/src/components/authentication/ViewAsUserChip.svelte @@ -28,17 +28,17 @@ $: project = $page.params.project; - - { - await updateMimickedJWT(queryClient, org, project, null); - errorStore.reset(); - }} - active={open} - > -
- + + + { + await updateMimickedJWT(queryClient, org, project, null); + errorStore.reset(); + }} + active={open} + > +
Viewing as {$viewAsUserStore.email} @@ -51,17 +51,17 @@
- - - close(undefined)} - /> - -
- - Clear view - -
+
+ + Clear view + +
+ + + close(undefined)} + /> +
From 3abb9bb8f568373b6852b9f98b3d29a0cb3807ef Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 18:44:16 -0400 Subject: [PATCH 20/33] Add checkmark to denote active user --- .../components/authentication/ViewAsUserPopover.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web-admin/src/components/authentication/ViewAsUserPopover.svelte b/web-admin/src/components/authentication/ViewAsUserPopover.svelte index e4e59ec7807..23457fad8cb 100644 --- a/web-admin/src/components/authentication/ViewAsUserPopover.svelte +++ b/web-admin/src/components/authentication/ViewAsUserPopover.svelte @@ -1,4 +1,6 @@ From 88a69f3548fc0d967640d3a3b6266ebb10540be1 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Tue, 12 Sep 2023 22:37:42 -0400 Subject: [PATCH 23/33] Better variable names --- .../[organization]/[project]/[dashboard]/+page.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte index 44f8235cd0d..0ac8a641014 100644 --- a/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte +++ b/web-admin/src/routes/[organization]/[project]/[dashboard]/+page.svelte @@ -41,8 +41,8 @@ $: isProjectErrored = $projectDeploymentStatus.data === V1DeploymentStatus.DEPLOYMENT_STATUS_ERROR; - $: isProjectUpdating = isProjectPending || isProjectReconciling; - $: isProjectStatic = isProjectOK || isProjectErrored; + $: isProjectBuilding = isProjectPending || isProjectReconciling; + $: isProjectBuilt = isProjectOK || isProjectErrored; let isProjectOK: boolean; @@ -76,7 +76,7 @@ // isDashboardErrored // We'll reinstate this case once we integrate the new Reconcile // If no dashboard is found, show a 404 page - $: if (isProjectStatic && isDashboardNotFound) { + $: if (isProjectBuilt && isDashboardNotFound) { errorStore.set({ statusCode: 404, header: "Dashboard not found", @@ -92,7 +92,7 @@ -{#if isProjectUpdating && isDashboardNotFound} +{#if isProjectBuilding && isDashboardNotFound} {:else if isDashboardOK} From 0126cbe87bc4f9a662677f175cb7f505779db7fb Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 10:50:35 -0400 Subject: [PATCH 24/33] Move code to `features/view-as-user` --- web-admin/src/components/authentication/UserButton.svelte | 2 +- web-admin/src/components/navigation/TopNavigationBar.svelte | 4 ++-- .../view-as-user}/ViewAsUserChip.svelte | 2 +- .../view-as-user}/ViewAsUserMenuItem.svelte | 0 .../view-as-user}/ViewAsUserPopover.svelte | 2 +- .../view-as-user/clearViewedAsUserAfterNavigate.ts} | 4 ++-- .../view-as-user}/viewAsUserStore.ts | 0 web-admin/src/routes/+layout.svelte | 4 ++-- web-admin/src/routes/[organization]/[project]/+layout.svelte | 2 +- .../dashboards/granular-access-policies/updateMimickedJWT.ts | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) rename web-admin/src/{components/authentication => features/view-as-user}/ViewAsUserChip.svelte (97%) rename web-admin/src/{components/authentication => features/view-as-user}/ViewAsUserMenuItem.svelte (100%) rename web-admin/src/{components/authentication => features/view-as-user}/ViewAsUserPopover.svelte (97%) rename web-admin/src/{components/authentication/clearMimickedUserAfterNavigate.ts => features/view-as-user/clearViewedAsUserAfterNavigate.ts} (89%) rename web-admin/src/{components/authentication => features/view-as-user}/viewAsUserStore.ts (100%) diff --git a/web-admin/src/components/authentication/UserButton.svelte b/web-admin/src/components/authentication/UserButton.svelte index eb173fa7ee9..bc86f9ecb2b 100644 --- a/web-admin/src/components/authentication/UserButton.svelte +++ b/web-admin/src/components/authentication/UserButton.svelte @@ -11,8 +11,8 @@ import { createPopperActions } from "svelte-popperjs"; import { createAdminServiceGetCurrentUser } from "../../client"; import { ADMIN_URL } from "../../client/http-client"; + import ViewAsUserPopover from "../../features/view-as-user/ViewAsUserPopover.svelte"; import ProjectAccessControls from "../projects/ProjectAccessControls.svelte"; - import ViewAsUserPopover from "./ViewAsUserPopover.svelte"; const user = createAdminServiceGetCurrentUser(); diff --git a/web-admin/src/components/navigation/TopNavigationBar.svelte b/web-admin/src/components/navigation/TopNavigationBar.svelte index ed3eecdee9a..3e3d804d54b 100644 --- a/web-admin/src/components/navigation/TopNavigationBar.svelte +++ b/web-admin/src/components/navigation/TopNavigationBar.svelte @@ -4,10 +4,10 @@ import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; import { createAdminServiceGetCurrentUser } from "../../client"; + import ViewAsUserChip from "../../features/view-as-user/ViewAsUserChip.svelte"; + import { viewAsUserStore } from "../../features/view-as-user/viewAsUserStore"; import SignIn from "../authentication/SignIn.svelte"; import UserButton from "../authentication/UserButton.svelte"; - import ViewAsUserChip from "../authentication/ViewAsUserChip.svelte"; - import { viewAsUserStore } from "../authentication/viewAsUserStore"; import { isErrorStoreEmpty } from "../errors/error-store"; import Breadcrumbs from "./Breadcrumbs.svelte"; diff --git a/web-admin/src/components/authentication/ViewAsUserChip.svelte b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte similarity index 97% rename from web-admin/src/components/authentication/ViewAsUserChip.svelte rename to web-admin/src/features/view-as-user/ViewAsUserChip.svelte index e34932f586b..9b80342be87 100644 --- a/web-admin/src/components/authentication/ViewAsUserChip.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte @@ -11,7 +11,7 @@ import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; import { useQueryClient } from "@tanstack/svelte-query"; import { createPopperActions } from "svelte-popperjs"; - import { errorStore } from "../errors/error-store"; + import { errorStore } from "../../components/errors/error-store"; import ViewAsUserPopover from "./ViewAsUserPopover.svelte"; import { viewAsUserStore } from "./viewAsUserStore"; diff --git a/web-admin/src/components/authentication/ViewAsUserMenuItem.svelte b/web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte similarity index 100% rename from web-admin/src/components/authentication/ViewAsUserMenuItem.svelte rename to web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte diff --git a/web-admin/src/components/authentication/ViewAsUserPopover.svelte b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte similarity index 97% rename from web-admin/src/components/authentication/ViewAsUserPopover.svelte rename to web-admin/src/features/view-as-user/ViewAsUserPopover.svelte index 23457fad8cb..14cd4a1fa97 100644 --- a/web-admin/src/components/authentication/ViewAsUserPopover.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte @@ -8,7 +8,7 @@ import { matchSorter } from "match-sorter"; import { createEventDispatcher } from "svelte"; import { createAdminServiceSearchProjectUsers, V1User } from "../../client"; - import { errorStore } from "../errors/error-store"; + import { errorStore } from "../../components/errors/error-store"; import { viewAsUserStore } from "./viewAsUserStore"; export let organization: string; diff --git a/web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts similarity index 89% rename from web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts rename to web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts index cfb0ae02024..ea8f55c9c65 100644 --- a/web-admin/src/components/authentication/clearMimickedUserAfterNavigate.ts +++ b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts @@ -7,9 +7,9 @@ import { get } from "svelte/store"; import { viewAsUserStore } from "./viewAsUserStore"; /** - * Remove the mimicked user (if any) when navigating away from the Dashboard page + * Remove the viewed as user (if any) when navigating away from the Dashboard page */ -export function clearMimickedUserAfterNavigate(queryClient: QueryClient) { +export function clearViewedAsUserAfterNavigate(queryClient: QueryClient) { afterNavigate((nav) => { // Only applies if mimicking a user on the Dashboard page if (!get(viewAsUserStore) || !nav.from?.params?.dashboard) return; diff --git a/web-admin/src/components/authentication/viewAsUserStore.ts b/web-admin/src/features/view-as-user/viewAsUserStore.ts similarity index 100% rename from web-admin/src/components/authentication/viewAsUserStore.ts rename to web-admin/src/features/view-as-user/viewAsUserStore.ts diff --git a/web-admin/src/routes/+layout.svelte b/web-admin/src/routes/+layout.svelte index 14e0e82b123..f4e06f14a98 100644 --- a/web-admin/src/routes/+layout.svelte +++ b/web-admin/src/routes/+layout.svelte @@ -11,10 +11,10 @@ QueryClient, QueryClientProvider, } from "@tanstack/svelte-query"; - import { clearMimickedUserAfterNavigate } from "../components/authentication/clearMimickedUserAfterNavigate"; import { globalErrorCallback } from "../components/errors/error-utils"; import ErrorBoundary from "../components/errors/ErrorBoundary.svelte"; import TopNavigationBar from "../components/navigation/TopNavigationBar.svelte"; + import { clearViewedAsUserAfterNavigate } from "../features/view-as-user/clearViewedAsUserAfterNavigate"; const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -39,7 +39,7 @@ }); beforeNavigate(retainFeaturesFlags); - clearMimickedUserAfterNavigate(queryClient); + clearViewedAsUserAfterNavigate(queryClient); diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index b26ae8f2104..53fda2d656d 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -3,8 +3,8 @@ import { page } from "$app/stores"; import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte"; import { createAdminServiceGetDeploymentCredentials } from "../../../client"; - import { viewAsUserStore } from "../../../components/authentication/viewAsUserStore"; import { useProjectRuntime } from "../../../components/projects/selectors"; + import { viewAsUserStore } from "../../../features/view-as-user/viewAsUserStore"; $: projRuntime = useProjectRuntime( $page.params.organization, diff --git a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts b/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts index 1c42240826a..81ab7e68083 100644 --- a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts +++ b/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts @@ -5,7 +5,7 @@ import { V1GetProjectResponse, type V1User, } from "@rilldata/web-admin/client"; -import { viewAsUserStore } from "@rilldata/web-admin/components/authentication/viewAsUserStore"; +import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore"; import { invalidateAllMetricsViews } from "@rilldata/web-common/runtime-client/invalidation"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import type { QueryClient } from "@tanstack/svelte-query"; From ff4c407a0531cc86c8c1aa431bdedbe3c7ba15ef Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 11:05:51 -0400 Subject: [PATCH 25/33] Factor out a `ViewAsUserMenuItem` component --- .../authentication/UserButton.svelte | 36 ++++----------- .../view-as-user/ViewAsUserMenuItem.svelte | 45 ++++++++++++++++++- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/web-admin/src/components/authentication/UserButton.svelte b/web-admin/src/components/authentication/UserButton.svelte index bc86f9ecb2b..47a80335459 100644 --- a/web-admin/src/components/authentication/UserButton.svelte +++ b/web-admin/src/components/authentication/UserButton.svelte @@ -5,13 +5,12 @@ PopoverButton, PopoverPanel, } from "@rgossiaux/svelte-headlessui"; - import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; import { MenuItem } from "@rilldata/web-common/components/menu"; import Menu from "@rilldata/web-common/components/menu/core/Menu.svelte"; import { createPopperActions } from "svelte-popperjs"; import { createAdminServiceGetCurrentUser } from "../../client"; import { ADMIN_URL } from "../../client/http-client"; - import ViewAsUserPopover from "../../features/view-as-user/ViewAsUserPopover.svelte"; + import ViewAsUserMenuItem from "../../features/view-as-user/ViewAsUserMenuItem.svelte"; import ProjectAccessControls from "../projects/ProjectAccessControls.svelte"; const user = createAdminServiceGetCurrentUser(); @@ -27,7 +26,7 @@ const isDev = process.env.NODE_ENV === "development"; - // Position the first popover + // Position the Menu popover const [popperRef1, popperContent1] = createPopperActions(); const popperOptions1 = { placement: "bottom-end", @@ -35,13 +34,8 @@ modifiers: [{ name: "offset", options: { offset: [0, 4] } }], }; - // Position the nested popover + // Position the View As User popover const [popperRef2, popperContent2] = createPopperActions(); - const popperOptions = { - placement: "left-start", - strategy: "fixed", - modifiers: [{ name: "offset", options: { offset: [0, 4] } }], - }; @@ -64,28 +58,14 @@ project={$page.params.project} > - - - - View as - - - - - close1(undefined)} - /> - - + close1(undefined)} + /> {/if} + { // handleClose(); diff --git a/web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte b/web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte index 2e2e8969d4e..1633762bf38 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte @@ -1 +1,44 @@ - + + + + + + View as + + + + + dispatch("select-user")} + /> + + From be82da5e4442b01dea2dadd78ffc4f3b772ddcae Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 11:10:06 -0400 Subject: [PATCH 26/33] Use consistent "viewed as" terminology --- web-admin/src/features/view-as-user/ViewAsUserChip.svelte | 4 ++-- .../features/view-as-user/clearViewedAsUserAfterNavigate.ts | 4 ++-- .../src/features/view-as-user/updateViewedAsUser.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts => web-admin/src/features/view-as-user/updateViewedAsUser.ts (97%) diff --git a/web-admin/src/features/view-as-user/ViewAsUserChip.svelte b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte index 9b80342be87..07d35648291 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserChip.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte @@ -8,10 +8,10 @@ import { IconSpaceFixer } from "@rilldata/web-common/components/button"; import { Chip } from "@rilldata/web-common/components/chip"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; - import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; import { useQueryClient } from "@tanstack/svelte-query"; import { createPopperActions } from "svelte-popperjs"; import { errorStore } from "../../components/errors/error-store"; + import { updateViewedAsUser } from "./updateViewedAsUser"; import ViewAsUserPopover from "./ViewAsUserPopover.svelte"; import { viewAsUserStore } from "./viewAsUserStore"; @@ -33,7 +33,7 @@ { - await updateMimickedJWT(queryClient, org, project, null); + await updateViewedAsUser(queryClient, org, project, null); errorStore.reset(); }} active={open} diff --git a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts index ea8f55c9c65..d0a668cf722 100644 --- a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts +++ b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts @@ -1,5 +1,5 @@ import { afterNavigate } from "$app/navigation"; -import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; +import { updateViewedAsUser } from "@rilldata/web-admin/features/view-as-user/updateViewedAsUser"; import { invalidateRuntimeQueries } from "@rilldata/web-common/runtime-client/invalidation"; import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import type { QueryClient } from "@tanstack/svelte-query"; @@ -16,7 +16,7 @@ export function clearViewedAsUserAfterNavigate(queryClient: QueryClient) { // If remaining within the project, set the admin's JWT if (!nav.to.params.dashboard && nav.to.params.project) { - updateMimickedJWT( + updateViewedAsUser( queryClient, nav.to.params.organization, nav.to.params.project, diff --git a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts b/web-admin/src/features/view-as-user/updateViewedAsUser.ts similarity index 97% rename from web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts rename to web-admin/src/features/view-as-user/updateViewedAsUser.ts index 81ab7e68083..8d6f62a1c2f 100644 --- a/web-common/src/features/dashboards/granular-access-policies/updateMimickedJWT.ts +++ b/web-admin/src/features/view-as-user/updateViewedAsUser.ts @@ -11,7 +11,7 @@ import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; import type { QueryClient } from "@tanstack/svelte-query"; import { get } from "svelte/store"; -export async function updateMimickedJWT( +export async function updateViewedAsUser( queryClient: QueryClient, organization: string, project: string, From 543bf3b3caffd6020527d5a69505a37662b7462f Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 11:13:44 -0400 Subject: [PATCH 27/33] Use consistent "view as" terminology (cont.) --- web-admin/src/features/view-as-user/ViewAsUserPopover.svelte | 5 ++--- .../features/view-as-user/clearViewedAsUserAfterNavigate.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte index 14cd4a1fa97..4a1984dddd4 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte @@ -3,12 +3,12 @@ import Spacer from "@rilldata/web-common/components/icons/Spacer.svelte"; import { Menu, MenuItem } from "@rilldata/web-common/components/menu"; import { Search } from "@rilldata/web-common/components/search"; - import { updateMimickedJWT } from "@rilldata/web-common/features/dashboards/granular-access-policies/updateMimickedJWT"; import { useQueryClient } from "@tanstack/svelte-query"; import { matchSorter } from "match-sorter"; import { createEventDispatcher } from "svelte"; import { createAdminServiceSearchProjectUsers, V1User } from "../../client"; import { errorStore } from "../../components/errors/error-store"; + import { updateViewedAsUser } from "./updateViewedAsUser"; import { viewAsUserStore } from "./viewAsUserStore"; export let organization: string; @@ -25,8 +25,7 @@ const queryClient = useQueryClient(); async function viewAsUser(user: V1User) { - viewAsUserStore.set(user); - await updateMimickedJWT(queryClient, organization, project, user); + await updateViewedAsUser(queryClient, organization, project, user); errorStore.reset(); dispatch("select"); } diff --git a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts index d0a668cf722..fc7b1e1af8a 100644 --- a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts +++ b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts @@ -11,7 +11,7 @@ import { viewAsUserStore } from "./viewAsUserStore"; */ export function clearViewedAsUserAfterNavigate(queryClient: QueryClient) { afterNavigate((nav) => { - // Only applies if mimicking a user on the Dashboard page + // Only applies if viewing as a user on the Dashboard page if (!get(viewAsUserStore) || !nav.from?.params?.dashboard) return; // If remaining within the project, set the admin's JWT From c50735f3f81d15406d8dd9df356d417e99088a81 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 11:41:46 -0400 Subject: [PATCH 28/33] Clean up actions --- .../view-as-user/ViewAsUserChip.svelte | 4 +- .../view-as-user/ViewAsUserPopover.svelte | 4 +- .../view-as-user/clearViewedAsUser.ts | 71 +++++++++++++++++++ .../clearViewedAsUserAfterNavigate.ts | 37 ---------- .../features/view-as-user/setViewedAsUser.ts | 39 ++++++++++ .../view-as-user/updateViewedAsUser.ts | 50 ------------- web-admin/src/routes/+layout.svelte | 2 +- 7 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 web-admin/src/features/view-as-user/clearViewedAsUser.ts delete mode 100644 web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts create mode 100644 web-admin/src/features/view-as-user/setViewedAsUser.ts delete mode 100644 web-admin/src/features/view-as-user/updateViewedAsUser.ts diff --git a/web-admin/src/features/view-as-user/ViewAsUserChip.svelte b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte index 07d35648291..559343c4f07 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserChip.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserChip.svelte @@ -11,7 +11,7 @@ import { useQueryClient } from "@tanstack/svelte-query"; import { createPopperActions } from "svelte-popperjs"; import { errorStore } from "../../components/errors/error-store"; - import { updateViewedAsUser } from "./updateViewedAsUser"; + import { clearViewedAsUserWithinProject } from "./clearViewedAsUser"; import ViewAsUserPopover from "./ViewAsUserPopover.svelte"; import { viewAsUserStore } from "./viewAsUserStore"; @@ -33,7 +33,7 @@ { - await updateViewedAsUser(queryClient, org, project, null); + await clearViewedAsUserWithinProject(queryClient, org, project); errorStore.reset(); }} active={open} diff --git a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte index 4a1984dddd4..c283ff92bd1 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte @@ -8,7 +8,7 @@ import { createEventDispatcher } from "svelte"; import { createAdminServiceSearchProjectUsers, V1User } from "../../client"; import { errorStore } from "../../components/errors/error-store"; - import { updateViewedAsUser } from "./updateViewedAsUser"; + import { setViewedAsUser } from "./setViewedAsUser"; import { viewAsUserStore } from "./viewAsUserStore"; export let organization: string; @@ -25,7 +25,7 @@ const queryClient = useQueryClient(); async function viewAsUser(user: V1User) { - await updateViewedAsUser(queryClient, organization, project, user); + await setViewedAsUser(queryClient, organization, project, user); errorStore.reset(); dispatch("select"); } diff --git a/web-admin/src/features/view-as-user/clearViewedAsUser.ts b/web-admin/src/features/view-as-user/clearViewedAsUser.ts new file mode 100644 index 00000000000..a889171ce7f --- /dev/null +++ b/web-admin/src/features/view-as-user/clearViewedAsUser.ts @@ -0,0 +1,71 @@ +import { afterNavigate } from "$app/navigation"; +import { + invalidateAllMetricsViews, + invalidateRuntimeQueries, +} from "@rilldata/web-common/runtime-client/invalidation"; +import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; +import type { QueryClient } from "@tanstack/svelte-query"; +import { get } from "svelte/store"; +import { + adminServiceGetProject, + getAdminServiceGetProjectQueryKey, + type V1GetProjectResponse, +} from "../../client"; +import { viewAsUserStore } from "./viewAsUserStore"; + +/** + * Remove the viewed as user (if any) when navigating away from the Dashboard page + */ +export function clearViewedAsUserAfterNavigate(queryClient: QueryClient) { + afterNavigate((nav) => { + // Only proceed if Viewing As a user on the Dashboard page + if (!get(viewAsUserStore) || !nav.from?.params?.dashboard) return; + + // If staying within the project, set the admin's JWT + if (!nav.to.params.dashboard && nav.to.params.project) { + clearViewedAsUserWithinProject( + queryClient, + nav.to.params.organization, + nav.to.params.project + ); + } + + // If leaving a project, clear the JWT outright + if (!nav.to.params.dashboard && !nav.to.params.project) { + clearViewedAsUserOutsideProject(queryClient); + } + }); +} + +export async function clearViewedAsUserWithinProject( + queryClient: QueryClient, + organization: string, + project: string +) { + viewAsUserStore.set(null); + + // Get the admin's original JWT from the `GetProject` call + const projResp = await queryClient.fetchQuery({ + queryKey: getAdminServiceGetProjectQueryKey(organization, project), + queryFn: () => adminServiceGetProject(organization, project), + }); + const jwt = projResp.jwt; + + runtime.update((runtimeState) => { + runtimeState.jwt = jwt; + return runtimeState; + }); + + await invalidateAllMetricsViews(queryClient, get(runtime).instanceId); +} + +async function clearViewedAsUserOutsideProject(queryClient: QueryClient) { + viewAsUserStore.set(null); + + runtime.update((runtimeState) => { + runtimeState.jwt = null; + return runtimeState; + }); + + await invalidateRuntimeQueries(queryClient); +} diff --git a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts b/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts deleted file mode 100644 index fc7b1e1af8a..00000000000 --- a/web-admin/src/features/view-as-user/clearViewedAsUserAfterNavigate.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { afterNavigate } from "$app/navigation"; -import { updateViewedAsUser } from "@rilldata/web-admin/features/view-as-user/updateViewedAsUser"; -import { invalidateRuntimeQueries } from "@rilldata/web-common/runtime-client/invalidation"; -import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; -import type { QueryClient } from "@tanstack/svelte-query"; -import { get } from "svelte/store"; -import { viewAsUserStore } from "./viewAsUserStore"; - -/** - * Remove the viewed as user (if any) when navigating away from the Dashboard page - */ -export function clearViewedAsUserAfterNavigate(queryClient: QueryClient) { - afterNavigate((nav) => { - // Only applies if viewing as a user on the Dashboard page - if (!get(viewAsUserStore) || !nav.from?.params?.dashboard) return; - - // If remaining within the project, set the admin's JWT - if (!nav.to.params.dashboard && nav.to.params.project) { - updateViewedAsUser( - queryClient, - nav.to.params.organization, - nav.to.params.project, - null - ); - } - - // If leaving a project, clear the JWT - if (!nav.to.params.dashboard && !nav.to.params.project) { - viewAsUserStore.set(null); - runtime.update((runtimeState) => { - runtimeState.jwt = null; - return runtimeState; - }); - invalidateRuntimeQueries(queryClient); - } - }); -} diff --git a/web-admin/src/features/view-as-user/setViewedAsUser.ts b/web-admin/src/features/view-as-user/setViewedAsUser.ts new file mode 100644 index 00000000000..cd720646f76 --- /dev/null +++ b/web-admin/src/features/view-as-user/setViewedAsUser.ts @@ -0,0 +1,39 @@ +import { + adminServiceGetDeploymentCredentials, + type V1User, +} from "@rilldata/web-admin/client"; +import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore"; +import { invalidateAllMetricsViews } from "@rilldata/web-common/runtime-client/invalidation"; +import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; +import type { QueryClient } from "@tanstack/svelte-query"; +import { get } from "svelte/store"; + +export async function setViewedAsUser( + queryClient: QueryClient, + organization: string, + project: string, + user: V1User +) { + viewAsUserStore.set(user); + + let jwt: string = null; + try { + const jwtResp = await adminServiceGetDeploymentCredentials( + organization, + project, + { + userId: user.id, + } + ); + jwt = jwtResp.jwt; + } catch (e) { + // no-op + } + + runtime.update((runtimeState) => { + runtimeState.jwt = jwt; + return runtimeState; + }); + + invalidateAllMetricsViews(queryClient, get(runtime).instanceId); +} diff --git a/web-admin/src/features/view-as-user/updateViewedAsUser.ts b/web-admin/src/features/view-as-user/updateViewedAsUser.ts deleted file mode 100644 index 8d6f62a1c2f..00000000000 --- a/web-admin/src/features/view-as-user/updateViewedAsUser.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - adminServiceGetDeploymentCredentials, - adminServiceGetProject, - getAdminServiceGetProjectQueryKey, - V1GetProjectResponse, - type V1User, -} from "@rilldata/web-admin/client"; -import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore"; -import { invalidateAllMetricsViews } from "@rilldata/web-common/runtime-client/invalidation"; -import { runtime } from "@rilldata/web-common/runtime-client/runtime-store"; -import type { QueryClient } from "@tanstack/svelte-query"; -import { get } from "svelte/store"; - -export async function updateViewedAsUser( - queryClient: QueryClient, - organization: string, - project: string, - user: V1User | null -) { - viewAsUserStore.set(user); - let jwt: string = null; - - if (user !== null) { - try { - const jwtResp = await adminServiceGetDeploymentCredentials( - organization, - project, - { - userId: user.id, - } - ); - jwt = jwtResp.jwt; - } catch (e) { - // no-op - } - } else { - // Get the admin's JWT from `GetProject` call - const projResp = await queryClient.fetchQuery({ - queryKey: getAdminServiceGetProjectQueryKey(organization, project), - queryFn: () => adminServiceGetProject(organization, project), - }); - jwt = projResp.jwt; - } - - runtime.update((runtimeState) => { - runtimeState.jwt = jwt; - return runtimeState; - }); - return invalidateAllMetricsViews(queryClient, get(runtime).instanceId); -} diff --git a/web-admin/src/routes/+layout.svelte b/web-admin/src/routes/+layout.svelte index f4e06f14a98..21a3bc86d2a 100644 --- a/web-admin/src/routes/+layout.svelte +++ b/web-admin/src/routes/+layout.svelte @@ -14,7 +14,7 @@ import { globalErrorCallback } from "../components/errors/error-utils"; import ErrorBoundary from "../components/errors/ErrorBoundary.svelte"; import TopNavigationBar from "../components/navigation/TopNavigationBar.svelte"; - import { clearViewedAsUserAfterNavigate } from "../features/view-as-user/clearViewedAsUserAfterNavigate"; + import { clearViewedAsUserAfterNavigate } from "../features/view-as-user/clearViewedAsUser"; const queryClient = new QueryClient({ queryCache: new QueryCache({ From b8a9bb84bd1f537900ca5252373279cb10f1cbb5 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 12:01:47 -0400 Subject: [PATCH 29/33] Nits --- web-admin/src/components/errors/error-utils.ts | 2 +- .../features/view-as-user/ViewAsUserPopover.svelte | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/web-admin/src/components/errors/error-utils.ts b/web-admin/src/components/errors/error-utils.ts index b5f1160aae3..0f70520f264 100644 --- a/web-admin/src/components/errors/error-utils.ts +++ b/web-admin/src/components/errors/error-utils.ts @@ -25,7 +25,7 @@ export function globalErrorCallback(error: AxiosError): void { // Special handling for some errors on the Dashboard page if (isDashboardPage) { // If a dashboard wasn't found, let +page.svelte handle the error. - // Because the project may be reconciling, in which case we should show a loading spinner not a 404. + // Because the project may be reconciling, in which case we want to show a loading spinner not a 404. if ( error.response.status === 404 && (error.response.data as RpcStatus).message === "not found" diff --git a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte index c283ff92bd1..8e392713271 100644 --- a/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte +++ b/web-admin/src/features/view-as-user/ViewAsUserPopover.svelte @@ -14,7 +14,7 @@ export let organization: string; export let project: string; - // Note: this will break down if there are more than 1000 users in a project + // Note: this approach will break down if/when there are more than 1000 users in a project $: projectUsers = createAdminServiceSearchProjectUsers( organization, project, @@ -24,7 +24,7 @@ const dispatch = createEventDispatcher(); const queryClient = useQueryClient(); - async function viewAsUser(user: V1User) { + async function handleViewAsUser(user: V1User) { await setViewedAsUser(queryClient, organization, project, user); errorStore.reset(); dispatch("select"); @@ -38,11 +38,6 @@ let searchText = ""; $: clientSideUsers = $projectUsers.data?.users ?? []; - // For testing: repeat the users array X times - // $: clientSideUsers = - // $projectUsers.data?.users.flatMap((users) => - // Array.from({ length: 10 }, () => users) - // ) ?? []; $: visibleUsers = searchText ? matchSorter(clientSideUsers, searchText, { keys: ["email"] }) @@ -75,7 +70,7 @@ icon animateSelect={false} focusOnMount={false} - on:select={() => viewAsUser(user)} + on:select={() => handleViewAsUser(user)} > {#if user === $viewAsUserStore} From cdbadd9b4ce432dcf2153699526755709120d280 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 12:03:24 -0400 Subject: [PATCH 30/33] Bug fix --- web-admin/src/features/view-as-user/setViewedAsUser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-admin/src/features/view-as-user/setViewedAsUser.ts b/web-admin/src/features/view-as-user/setViewedAsUser.ts index cd720646f76..a9e091f8231 100644 --- a/web-admin/src/features/view-as-user/setViewedAsUser.ts +++ b/web-admin/src/features/view-as-user/setViewedAsUser.ts @@ -35,5 +35,5 @@ export async function setViewedAsUser( return runtimeState; }); - invalidateAllMetricsViews(queryClient, get(runtime).instanceId); + await invalidateAllMetricsViews(queryClient, get(runtime).instanceId); } From 0891f614b99f88a00ff45e6691e0b50bc4f71a81 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 12:05:08 -0400 Subject: [PATCH 31/33] Use query cache --- .../features/view-as-user/setViewedAsUser.ts | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/web-admin/src/features/view-as-user/setViewedAsUser.ts b/web-admin/src/features/view-as-user/setViewedAsUser.ts index a9e091f8231..b6f6fb4d362 100644 --- a/web-admin/src/features/view-as-user/setViewedAsUser.ts +++ b/web-admin/src/features/view-as-user/setViewedAsUser.ts @@ -1,5 +1,7 @@ import { adminServiceGetDeploymentCredentials, + getAdminServiceGetDeploymentCredentialsQueryKey, + V1GetDeploymentCredentialsResponse, type V1User, } from "@rilldata/web-admin/client"; import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore"; @@ -16,19 +18,21 @@ export async function setViewedAsUser( ) { viewAsUserStore.set(user); - let jwt: string = null; - try { - const jwtResp = await adminServiceGetDeploymentCredentials( - organization, - project, - { - userId: user.id, - } - ); - jwt = jwtResp.jwt; - } catch (e) { - // no-op - } + const jwtResp = + await queryClient.fetchQuery({ + queryKey: getAdminServiceGetDeploymentCredentialsQueryKey( + organization, + project, + { + userId: user.id, + } + ), + queryFn: () => + adminServiceGetDeploymentCredentials(organization, project, { + userId: user.id, + }), + }); + const jwt = jwtResp.jwt; runtime.update((runtimeState) => { runtimeState.jwt = jwt; From a4e72ae07d69938d3c989075877f933021e7f0ff Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 12:08:07 -0400 Subject: [PATCH 32/33] Remove unneccessary query observer --- .../[organization]/[project]/+layout.svelte | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 53fda2d656d..38d0cc8531a 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -2,9 +2,7 @@ import { goto } from "$app/navigation"; import { page } from "$app/stores"; import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte"; - import { createAdminServiceGetDeploymentCredentials } from "../../../client"; import { useProjectRuntime } from "../../../components/projects/selectors"; - import { viewAsUserStore } from "../../../features/view-as-user/viewAsUserStore"; $: projRuntime = useProjectRuntime( $page.params.organization, @@ -17,27 +15,13 @@ // Redirect any nested routes (notably dashboards) to the project page goto(`/${$page.params.organization}/${$page.params.project}`); } - - // if viewAs is set (which only admins can configure), we need to update the runtime with the new jwt - $: deploymentCredsQuery = createAdminServiceGetDeploymentCredentials( - $page.params.organization, - $page.params.project, - { - userId: $viewAsUserStore?.id, - }, - { - query: { - enabled: $viewAsUserStore?.id !== undefined, - }, - } - ); {#if $projRuntime.data} From 302d95bf431e9f72dffcbf6b26ee6321a9525167 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Wed, 13 Sep 2023 12:22:31 -0400 Subject: [PATCH 33/33] Handle case when admin's JWT gets refreshed --- web-admin/src/routes/[organization]/[project]/+layout.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web-admin/src/routes/[organization]/[project]/+layout.svelte b/web-admin/src/routes/[organization]/[project]/+layout.svelte index 38d0cc8531a..7a25b5ff9e3 100644 --- a/web-admin/src/routes/[organization]/[project]/+layout.svelte +++ b/web-admin/src/routes/[organization]/[project]/+layout.svelte @@ -3,6 +3,7 @@ import { page } from "$app/stores"; import RuntimeProvider from "@rilldata/web-common/runtime-client/RuntimeProvider.svelte"; import { useProjectRuntime } from "../../../components/projects/selectors"; + import { viewAsUserStore } from "../../../features/view-as-user/viewAsUserStore"; $: projRuntime = useProjectRuntime( $page.params.organization, @@ -17,7 +18,9 @@ } -{#if $projRuntime.data} + +{#if $projRuntime.data && !$viewAsUserStore}