Skip to content

Commit

Permalink
Add "View as" button to Rill Cloud dashboards (#3047)
Browse files Browse the repository at this point in the history
* Refactor `UserButton`

* Add conditional 'View as' menu item

* Add `svelte-headlessui` and `svelte-popperjs`

* Add "View As" popover

* Acquire and set JWT

* Remove calls to `ListFiles` API

* Fix `svelte-check` w/ explicit type

* Add comment

* Patch `MenuItem` component

* Remove File API usage

* Handle chip removal

* Fix z-index on chip

* Clear error when switching users

* Switch back to admin's JWT

* Handle 404 from `GetCatalog`

* Add workaround for metrics view 401s; clean up

* Clear mimicked user after navigation

* Bugfix

* Make the whole chip clickable

* Add checkmark to denote active user

* Remove comment

* Edit 404 copy

* Better variable names

* Move code to `features/view-as-user`

* Factor out a `ViewAsUserMenuItem` component

* Use consistent "viewed as" terminology

* Use consistent "view as" terminology (cont.)

* Clean up actions

* Nits

* Bug fix

* Use query cache

* Remove unneccessary query observer

* Handle case when admin's JWT gets refreshed
  • Loading branch information
ericpgreen2 authored Sep 14, 2023
1 parent dff3a4a commit 26c6bad
Show file tree
Hide file tree
Showing 15 changed files with 462 additions and 43 deletions.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions web-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"devDependencies": {
"@fontsource/fira-mono": "^4.5.0",
"@playwright/test": "^1.25.0",
"@rgossiaux/svelte-headlessui": "^2.0.0",
"@sveltejs/adapter-static": "^1.0.0",
"@sveltejs/kit": "^1.5.0",
"@rilldata/svelte-query": "^4.29.20-0.0.1",
Expand All @@ -34,6 +35,7 @@
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.48.0",
"svelte-check": "^3.0.3",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^4.10.6",
"tailwindcss": "^3.2.7",
"tslib": "^2.3.1",
Expand Down
84 changes: 65 additions & 19 deletions web-admin/src/components/authentication/UserButton.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
<script lang="ts">
import SimpleActionMenu from "@rilldata/web-common/components/menu/wrappers/SimpleActionMenu.svelte";
import { page } from "$app/stores";
import {
Popover,
PopoverButton,
PopoverPanel,
} from "@rgossiaux/svelte-headlessui";
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 ViewAsUserMenuItem from "../../features/view-as-user/ViewAsUserMenuItem.svelte";
import ProjectAccessControls from "../projects/ProjectAccessControls.svelte";
const user = createAdminServiceGetCurrentUser();
Expand All @@ -15,23 +25,59 @@
}
const isDev = process.env.NODE_ENV === "development";
// Position the Menu popover
const [popperRef1, popperContent1] = createPopperActions();
const popperOptions1 = {
placement: "bottom-end",
strategy: "fixed",
modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
};
// Position the View As User popover
const [popperRef2, popperContent2] = createPopperActions();
</script>

<SimpleActionMenu
options={[
{ main: "Logout", callback: handleLogOut },
{ main: "Documentation", callback: handleDocumentation },
]}
let:toggleMenu
minWidth="0px"
distance={4}
>
<img
src={$user.data?.user?.photoUrl}
alt="avatar"
class="h-7 rounded-full cursor-pointer"
referrerpolicy={isDev ? "no-referrer" : ""}
on:click={toggleMenu}
on:keydown={toggleMenu}
/>
</SimpleActionMenu>
<Popover class="relative" let:close={close1}>
<PopoverButton use={[popperRef1]}>
<img
src={$user.data?.user?.photoUrl}
alt="avatar"
class="h-7 rounded-full cursor-pointer"
referrerpolicy={isDev ? "no-referrer" : ""}
/>
</PopoverButton>
<PopoverPanel
use={[popperRef2, [popperContent1, popperOptions1]]}
class="max-w-fit absolute z-[1000]"
>
<Menu minWidth="0px" focusOnMount={false}>
{#if $page.params.organization && $page.params.project && $page.params.dashboard}
<ProjectAccessControls
organization={$page.params.organization}
project={$page.params.project}
>
<svelte:fragment slot="manage-project">
<ViewAsUserMenuItem
popperContent={popperContent2}
on:select-user={() => close1(undefined)}
/>
</svelte:fragment>
</ProjectAccessControls>
{/if}

<MenuItem
on:select={() => {
// handleClose();
handleLogOut();
}}>Logout</MenuItem
>
<MenuItem
on:select={() => {
// handleClose();
handleDocumentation();
}}>Documentation</MenuItem
>
</Menu>
</PopoverPanel>
</Popover>
53 changes: 32 additions & 21 deletions web-admin/src/components/errors/error-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 want to 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 === 400 &&
(error.response.data as RpcStatus).message === "entry not found"
) {
// If Unauthorized, redirect to login page
if (error.response.status === 401) {
goto(`${ADMIN_URL}/auth/login?redirect=${window.origin}`);
return;
}

Expand Down
5 changes: 5 additions & 0 deletions web-admin/src/components/navigation/TopNavigationBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
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 { isErrorStoreEmpty } from "../errors/error-store";
Expand Down Expand Up @@ -38,6 +40,9 @@
<div />
{/if}
<div class="flex gap-x-3 items-center">
{#if $viewAsUserStore}
<ViewAsUserChip />
{/if}
<a
class="font-medium"
href="https://discord.com/invite/ngVV4KzEGv?utm_source=rill&utm_medium=rill-cloud-nav"
Expand Down
67 changes: 67 additions & 0 deletions web-admin/src/features/view-as-user/ViewAsUserChip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import { page } from "$app/stores";
import {
Popover,
PopoverButton,
PopoverPanel,
} from "@rgossiaux/svelte-headlessui";
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 { useQueryClient } from "@tanstack/svelte-query";
import { createPopperActions } from "svelte-popperjs";
import { errorStore } from "../../components/errors/error-store";
import { clearViewedAsUserWithinProject } from "./clearViewedAsUser";
import ViewAsUserPopover from "./ViewAsUserPopover.svelte";
import { viewAsUserStore } from "./viewAsUserStore";
// Position the popover
const [popperRef, popperContent] = createPopperActions();
const popperOptions = {
placement: "bottom-start",
strategy: "fixed",
modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
};
const queryClient = useQueryClient();
$: org = $page.params.organization;
$: project = $page.params.project;
</script>

<Popover let:close let:open>
<PopoverButton use={[popperRef]}>
<Chip
removable
on:remove={async () => {
await clearViewedAsUserWithinProject(queryClient, org, project);
errorStore.reset();
}}
active={open}
>
<div slot="body">
<div class="flex gap-x-2">
<div>
Viewing as <span class="font-bold">{$viewAsUserStore.email}</span>
</div>
<div class="flex items-center">
<IconSpaceFixer pullRight>
<div class="transition-transform" class:-rotate-180={open}>
<CaretDownIcon size="14px" />
</div>
</IconSpaceFixer>
</div>
</div>
</div>
<svelte:fragment slot="remove-tooltip">
<slot name="remove-tooltip-content">Clear view</slot>
</svelte:fragment>
</Chip>
</PopoverButton>
<PopoverPanel use={[[popperContent, popperOptions]]} class="z-[1000]">
<ViewAsUserPopover
organization={$page.params.organization}
project={$page.params.project}
on:select={() => close(undefined)}
/>
</PopoverPanel>
</Popover>
44 changes: 44 additions & 0 deletions web-admin/src/features/view-as-user/ViewAsUserMenuItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Modifier } from "@popperjs/core";
import {
Popover,
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 { createEventDispatcher } from "svelte";
import type { ContentAction } from "svelte-popperjs";
import ViewAsUserPopover from "../../features/view-as-user/ViewAsUserPopover.svelte";
export let popperContent: ContentAction<Partial<Modifier<any, any>>>;
const popperOptions = {
placement: "left-start",
strategy: "fixed",
modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
};
const dispatch = createEventDispatcher();
</script>

<Popover>
<PopoverButton class="w-full text-left">
<MenuItem animateSelect={false}>
View as
<CaretDownIcon
className="transform -rotate-90"
slot="right"
size="14px"
/>
</MenuItem>
</PopoverButton>
<PopoverPanel use={[[popperContent, popperOptions]]}>
<ViewAsUserPopover
organization={$page.params.organization}
project={$page.params.project}
on:select={() => dispatch("select-user")}
/>
</PopoverPanel>
</Popover>
Loading

1 comment on commit 26c6bad

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.