Skip to content

Commit

Permalink
Add "View As" popover
Browse files Browse the repository at this point in the history
  • Loading branch information
ericpgreen2 committed Sep 8, 2023
1 parent aac65ad commit 770d813
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 60 deletions.
139 changes: 79 additions & 60 deletions web-admin/src/components/authentication/UserButton.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<script lang="ts">
import { page } from "$app/stores";
import WithTogglableFloatingElement from "@rilldata/web-common/components/floating-element/WithTogglableFloatingElement.svelte";
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 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 ProjectAccessControls from "../projects/ProjectAccessControls.svelte";
import ViewAsUserPopover from "./ViewAsUserPopover.svelte";
const user = createAdminServiceGetCurrentUser();
let menuOpen = false;
function handleViewAs() {
console.log("open 'view as' UI");
}
function handleLogOut() {
const loginWithRedirect = `${ADMIN_URL}/auth/login?redirect=${window.location.origin}${window.location.pathname}`;
window.location.href = `${ADMIN_URL}/auth/logout?redirect=${loginWithRedirect}`;
Expand All @@ -25,60 +26,78 @@
}
const isDev = process.env.NODE_ENV === "development";
// Position the first popover
const [popperRef1, popperContent1] = createPopperActions();
const popperOptions1 = {
placement: "bottom-end",
strategy: "fixed",
modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
};
// Position the nested popover
const [popperRef2, popperContent2] = createPopperActions();
const popperOptions = {
placement: "left-start",
strategy: "fixed",
modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
};
</script>

<WithTogglableFloatingElement
distance={4}
location="bottom"
alignment="start"
let:handleClose
let:toggleFloatingElement={toggleMenu}
on:open={() => (menuOpen = true)}
on:close={() => (menuOpen = false)}
>
<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}
/>
<Menu
slot="floating-element"
minWidth="0px"
focusOnMount={false}
on:select-item={handleClose}
on:click-outside={handleClose}
on:escape={handleClose}
<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]"
>
{#if $page.params.organization && $page.params.project}
<ProjectAccessControls
organization={$page.params.organization}
project={$page.params.project}
>
<MenuItem
slot="manage-project"
on:select={() => {
handleClose();
handleViewAs();
}}
<Menu minWidth="0px" focusOnMount={false}>
{#if $page.params.organization && $page.params.project && $page.params.dashboard}
<ProjectAccessControls
organization={$page.params.organization}
project={$page.params.project}
>
View as
</MenuItem>
</ProjectAccessControls>
{/if}
<MenuItem
on:select={() => {
handleClose();
handleLogOut();
}}>Logout</MenuItem
>
<MenuItem
on:select={() => {
handleClose();
handleDocumentation();
}}>Documentation</MenuItem
>
</Menu>
</WithTogglableFloatingElement>
<svelte:fragment slot="manage-project">
<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={[[popperContent2, popperOptions]]}>
<ViewAsUserPopover
organization={$page.params.organization}
project={$page.params.project}
on:select={() => close1(undefined)}
/>
</PopoverPanel>
</Popover>
</svelte:fragment>
</ProjectAccessControls>
{/if}
<MenuItem
on:select={() => {
// handleClose();
handleLogOut();
}}>Logout</MenuItem
>
<MenuItem
on:select={() => {
// handleClose();
handleDocumentation();
}}>Documentation</MenuItem
>
</Menu>
</PopoverPanel>
</Popover>
60 changes: 60 additions & 0 deletions web-admin/src/components/authentication/ViewAsUserChip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<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 { createPopperActions } from "svelte-popperjs";
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] } }],
};
</script>

<Popover use={[popperRef]} let:close let:open>
<Chip
removable
on:remove={() => {
// updateMimickedJWT(queryClient, null);
viewAsUserStore.set(null);
}}
active={open}
>
<div slot="body">
<PopoverButton>
<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>
</PopoverButton>
<PopoverPanel use={[[popperContent, popperOptions]]}>
<ViewAsUserPopover
organization={$page.params.organization}
project={$page.params.project}
on:select={() => close(undefined)}
/>
</PopoverPanel>
</div>
<svelte:fragment slot="remove-tooltip">
<slot name="remove-tooltip-content">Clear view</slot>
</svelte:fragment>
</Chip>
</Popover>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- TODO: move code from `UserButton.svelte` -->
83 changes: 83 additions & 0 deletions web-admin/src/components/authentication/ViewAsUserPopover.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
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 { viewAsUserStore } from "./viewAsUserStore";
export let organization: string;
export let project: string;
// Note: this will break down if there are more than 1000 users in a project
$: projectUsers = createAdminServiceSearchProjectUsers(
organization,
project,
{ emailQuery: "%", pageSize: 1000, pageToken: undefined }
);
const dispatch = createEventDispatcher();
const queryClient = useQueryClient();
function viewAsUser(user: V1User) {
viewAsUserStore.set(user);
updateMimickedJWT(queryClient, organization, project, user);
dispatch("select");
}
let minWidth = "150px";
let maxWidth = "300px";
let minHeight = "150px";
let maxHeight = "190px";
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"] })
: clientSideUsers;
</script>

<Menu
focusOnMount={false}
{minWidth}
{maxWidth}
{minHeight}
{maxHeight}
paddingBottom={0}
paddingTop={1}
rounded={false}
on:click-outside
on:escape
>
<div class="px-2 pt-1 pb-2 text-[10px] text-gray-500 text-left">
Test your <a
target="_blank"
href="https://docs.rilldata.com/develop/security">security policies</a
> by viewing this project from the perspective of another user.
</div>
<Search bind:value={searchText} />
{#if visibleUsers.length > 0}
<div class="overflow-auto pb-1">
{#each visibleUsers as user}
<MenuItem
animateSelect={false}
focusOnMount={false}
on:select={() => viewAsUser(user)}
>
{user.email}
</MenuItem>
{/each}
</div>
{:else}
<div class="mt-5 ui-copy-disabled text-center">no results</div>
{/if}
</Menu>
4 changes: 4 additions & 0 deletions web-admin/src/components/authentication/viewAsUserStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { writable } from "svelte/store";
import type { V1User } from "../../client";

export const viewAsUserStore = writable<V1User | null>(null);
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 @@ -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";
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
adminServiceGetDeploymentCredentials,
type V1User,
} from "@rilldata/web-admin/client";
import { viewAsUserStore } from "@rilldata/web-admin/components/authentication/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 updateMimickedJWT(
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
}
}

// selectedMockUserJWT.set(jwt);
runtime.update((runtimeState) => {
runtimeState.jwt = jwt;
return runtimeState;
});
return invalidateAllMetricsViews(queryClient, get(runtime).instanceId);
}

0 comments on commit 770d813

Please sign in to comment.