Skip to content

Commit

Permalink
User management UI (#5793)
Browse files Browse the repository at this point in the history
* clean up org tabs

* org users table

* user groups

* capitalize

* delete user group action

* fix user group deletion

* rerender

* class tweaks

* add basic table component

* apply basic table to project resources table

* set user group role

* add then set role for user group

* conditional item or checkbox item based on role

* revoke user group role

* bug fix

* rename user group dialog

* rework forms in user group dialogs

* user composite cell

* lint

* lint

* lint

* add user to org

* remove user from org

* fix disabled

* add avatar component

* set role for org member user

* lint

* disallow current user to change role or remove themselves

* show org invites

* fix interface

* add member to user group

* allow adding user to user group for verified users

* organize dialog footer actions

* add users to usergroup

* wip before edit user group dialog

* pending invite copy

* update user round for avatar placeholder when no alt

* allow add to group for current user

* edit user group dialog, remove user from group

* lint

* remove rename user group dialog

* polish user group member users list

* pass current user email down

* update copy

* org groups table role cell, set or update role

* clean up

* org users table role cell

* lint

* rework add users

* lint

* basic table tweaks, widthPercent addition

* lint

* search in users table, empty text

* search in groups table

* nit

* lint

* clean up, hide remove for all-users

* lint

* pre combobox

* use combobox, search and add users in create group dialog

* remove unused, clean up for all-users group

* lint

* ability to add users to usergroup in edit group dialog

* remove adding users to usergroup in create group dialog

* add avatar circle list, polish typeahead

* lint

* filter out users that are already in the group

* lint

* danger text for remove text button

* clean up

* fix input styles in combobox

* remove unused

* try

* lint

* bump typecheck nonsvelte files

* add scrollable table config, random color wip

* comment

* clean up

* clean up

* clean up

* scope out usergroup changes

* comment

* remove collaborator role option

* use random color, non-gray, non-primary-secondary

* clean up

* Revert "bump typecheck nonsvelte files"

This reverts commit e18a898.

* add chevron to role change

* Reapply "bump typecheck nonsvelte files"

This reverts commit 4a5418e.

* horizontal line feedback

* lint

* text danger

* deterministic bg color client side

* comment out desc from settings

* noop comment

* Revert "comment out desc from settings"

This reverts commit 3e4d6e2.

* feedback

* update src alt conditionals from avatar

* feedback

* fix rebase

* avoid prop drilling, feedback
  • Loading branch information
lovincyrus authored Oct 11, 2024
1 parent dd0cd06 commit 756095f
Show file tree
Hide file tree
Showing 32 changed files with 2,000 additions and 187 deletions.
7 changes: 1 addition & 6 deletions web-admin/src/features/navigation/TopNavigationBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import { useReports } from "../scheduled-reports/selectors";
import {
isMetricsExplorerPage,
isOrganizationPage,
isProjectPage,
isPublicURLPage,
} from "./nav-utils";
Expand All @@ -56,7 +55,6 @@
$: onReportPage = !!report;
$: onMetricsExplorerPage = isMetricsExplorerPage($page);
$: onPublicURLPage = isPublicURLPage($page);
$: onOrgPage = isOrganizationPage($page);
$: loggedIn = !!$user.data?.user;
$: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/";
Expand Down Expand Up @@ -160,10 +158,7 @@
$: currentPath = [organization, project, dashboard, report || alert];
</script>

<div
class="flex items-center w-full pr-4 pl-2 py-1"
class:border-b={!onProjectPage && !onOrgPage}
>
<div class="flex items-center w-full pr-4 pl-2 py-1">
<!-- Left side -->
<a
href={rillLogoHref}
Expand Down
14 changes: 8 additions & 6 deletions web-admin/src/features/organizations/OrganizationTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
];
if (data.permissions.manageOrgMembers) {
// TODO: users page
tabs.push({
route: `/${organization}/-/users`,
label: "Users",
});
}
if (data.permissions.manageOrg) {
// TODO: once settings page is filled in we add these
// tabs.push({
// route: `/${organization}/-/settings`,
// label: "Settings",
// });
tabs.push({
route: `/${organization}/-/settings`,
label: "Settings",
});
}
return tabs;
Expand Down
199 changes: 199 additions & 0 deletions web-admin/src/features/organizations/users/AddUsersDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<script lang="ts">
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@rilldata/web-common/components/dialog-v2";
import { Button } from "@rilldata/web-common/components/button/index.js";
import { defaults, superForm } from "sveltekit-superforms";
import { yup } from "sveltekit-superforms/adapters";
import { array, object, string } from "yup";
import MultiInput from "@rilldata/web-common/components/forms/MultiInput.svelte";
import UserRoleSelect from "@rilldata/web-admin/features/projects/user-invite/UserRoleSelect.svelte";
import { RFC5322EmailRegex } from "@rilldata/web-common/components/forms/validation";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { useQueryClient } from "@tanstack/svelte-query";
import {
createAdminServiceAddOrganizationMemberUser,
getAdminServiceListOrganizationInvitesQueryKey,
getAdminServiceListOrganizationMemberUsersQueryKey,
} from "@rilldata/web-admin/client";
import { page } from "$app/stores";
export let open = false;
export let email: string;
export let role: string;
export let isSuperUser: boolean;
$: organization = $page.params.organization;
const queryClient = useQueryClient();
const addOrganizationMemberUser =
createAdminServiceAddOrganizationMemberUser();
async function handleCreate(
newEmail: string,
newRole: string,
isSuperUser: boolean = false,
) {
try {
await $addOrganizationMemberUser.mutateAsync({
organization: organization,
data: {
email: newEmail,
role: newRole,
superuserForceAccess: isSuperUser,
},
});
await queryClient.invalidateQueries(
getAdminServiceListOrganizationMemberUsersQueryKey(organization),
);
await queryClient.invalidateQueries(
getAdminServiceListOrganizationInvitesQueryKey(organization),
);
email = "";
role = "";
isSuperUser = false;
open = false;
eventBus.emit("notification", { message: "User added to organization" });
} catch (error) {
console.error("Error adding user to organization", error);
eventBus.emit("notification", {
message: "Error adding user to organization",
type: "error",
});
}
}
const formId = "add-user-form";
const initialValues: {
emails: string[];
role: string;
} = {
emails: [""],
role: "viewer",
};
const schema = yup(
object({
emails: array(
string().matches(RFC5322EmailRegex, {
excludeEmptyString: true,
message: "Invalid email",
}),
), // yup's email regex is too simple
role: string().required(),
}),
);
const { form, errors, enhance, submit, submitting } = superForm(
defaults(initialValues, schema),
{
SPA: true,
validators: schema,
async onUpdate({ form }) {
if (!form.valid) return;
const values = form.data;
const emails = values.emails.map((e) => e.trim()).filter(Boolean);
if (emails.length === 0) return;
const succeeded = [];
let errored = false;
await Promise.all(
emails.map(async (email) => {
try {
await handleCreate(email, values.role, isSuperUser);
succeeded.push(email);
} catch (error) {
console.error("Error adding user to organization", error);
errored = true;
}
}),
);
eventBus.emit("notification", {
type: "success",
message: `Invited ${succeeded.length} ${succeeded.length === 1 ? "person" : "people"} as ${values.role}`,
});
if (errored) {
eventBus.emit("notification", {
type: "error",
message:
"Some invitations could not be sent. Please check the email addresses and try again.",
});
}
},
validationMethod: "oninput",
},
);
$: hasInvalidEmails = $form.emails.some(
(e, i) => e.length > 0 && $errors.emails?.[i] !== undefined,
);
</script>

<Dialog
bind:open
onOutsideClick={(e) => {
e.preventDefault();
open = false;
email = "";
role = "";
isSuperUser = false;
}}
onOpenChange={(open) => {
if (!open) {
email = "";
role = "";
isSuperUser = false;
}
}}
>
<DialogTrigger asChild>
<div class="hidden"></div>
</DialogTrigger>
<DialogContent class="translate-y-[-200px]">
<DialogHeader>
<DialogTitle>Add users</DialogTitle>
</DialogHeader>
<form
id={formId}
on:submit|preventDefault={submit}
class="w-full"
use:enhance
>
<MultiInput
id="emails"
placeholder="Add emails, separated by commas"
contentClassName="relative"
bind:values={$form.emails}
errors={$errors.emails}
singular="email"
plural="emails"
>
<div slot="within-input" class="flex items-center h-full">
<UserRoleSelect bind:value={$form.role} />
</div>
<svelte:fragment slot="beside-input" let:hasSomeValue>
<Button
submitForm
type="primary"
form={formId}
loading={$submitting}
disabled={hasInvalidEmails || !hasSomeValue}
forcedStyle="height: 32px !important;"
>
Invite
</Button>
</svelte:fragment>
</MultiInput>
</form>
</DialogContent>
</Dialog>
28 changes: 28 additions & 0 deletions web-admin/src/features/organizations/users/AvatarCircleList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import Avatar from "@rilldata/web-common/components/avatar/Avatar.svelte";
import { getRandomColor } from "@rilldata/web-common/features/themes/color-config";
export let name: string;
export let email: string;
export let isCurrentUser: boolean = false;
export let pendingAcceptance: boolean = false;
</script>

<div class="flex items-center gap-2">
<Avatar
size="h-7 w-7"
alt={pendingAcceptance ? null : name}
bgColor={getRandomColor(email)}
/>
<div class="flex flex-col text-left">
<span class="text-sm font-medium text-gray-900">
{name}
<span class="text-gray-500 font-normal">
{isCurrentUser ? "(You)" : ""}
</span>
</span>
<span class="text-xs text-gray-500"
>{pendingAcceptance ? "Pending invitation" : email}</span
>
</div>
</div>
Loading

1 comment on commit 756095f

@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.