-
Notifications
You must be signed in to change notification settings - Fork 178
Groups page project column #8883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
d147506
8ab307c
65211d5
37c47af
72a8e8a
13fa798
608d4a9
11b0e2e
fc0203a
9704472
bdf69a4
0f8d16a
dea1a98
7ee383f
c468596
482218b
e51e778
6a40817
62c6822
311d4eb
c48ee92
386877d
0b9e443
a4a913f
48911b9
312def3
5d87609
e9b78f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
… groups - Fixed race condition in collecting project access results - Added 'Add Project' dropdown to allow adding groups to projects - Shows a '+' button to add group to available projects - Uses ProjectUserRoles.Viewer as default role when adding - Invalidates queries and refreshes data after adding Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,30 @@ | |
| import { | ||
| adminServiceListProjectsForOrganization, | ||
| adminServiceListProjectMemberUsergroups, | ||
| createAdminServiceAddProjectMemberUsergroup, | ||
| getAdminServiceListProjectMemberUsergroupsQueryKey, | ||
| type V1Project, | ||
| } from "@rilldata/web-admin/client"; | ||
| import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; | ||
| import CaretUpIcon from "@rilldata/web-common/components/icons/CaretUpIcon.svelte"; | ||
| import { Plus } from "lucide-svelte"; | ||
| import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; | ||
| import { useQueryClient } from "@tanstack/svelte-query"; | ||
| import { ProjectUserRoles } from "@rilldata/web-common/features/users/roles"; | ||
|
|
||
| export let organization: string; | ||
| export let groupName: string; | ||
|
|
||
| const queryClient = useQueryClient(); | ||
| const addProjectMemberUsergroup = createAdminServiceAddProjectMemberUsergroup(); | ||
|
|
||
| let isDropdownOpen = false; | ||
| let isAddProjectDropdownOpen = false; | ||
| let isPending = true; | ||
| let isAddingProject = false; | ||
| let accessibleProjects: V1Project[] = []; | ||
| let allProjects: V1Project[] = []; | ||
| let error: string | null = null; | ||
| let hasLoaded = false; | ||
|
|
||
| async function loadProjectsForGroup() { | ||
|
|
@@ -27,11 +39,9 @@ | |
| try { | ||
| const projectsResponse = | ||
| await adminServiceListProjectsForOrganization(organization); | ||
| const allProjects = projectsResponse.projects ?? []; | ||
|
|
||
| const projectsWithAccess: V1Project[] = []; | ||
| allProjects = projectsResponse.projects ?? []; | ||
|
|
||
| await Promise.all( | ||
| const projectAccessResults = await Promise.all( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this really the best way to get projects? We should add a new API that fetches all projects accessible by the user group.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @begelundmuller, another one here! |
||
| allProjects.map(async (project) => { | ||
| try { | ||
| const usergroupsResponse = | ||
|
|
@@ -41,16 +51,16 @@ | |
| ); | ||
| const members = usergroupsResponse.members ?? []; | ||
| const hasAccess = members.some((m) => m.groupName === groupName); | ||
| if (hasAccess) { | ||
| projectsWithAccess.push(project); | ||
| } | ||
| return { project, hasAccess }; | ||
| } catch { | ||
| // If we can't check this project, skip it | ||
| return { project, hasAccess: false }; | ||
| } | ||
| }), | ||
| ); | ||
|
|
||
| accessibleProjects = projectsWithAccess; | ||
| accessibleProjects = projectAccessResults | ||
| .filter((r) => r.hasAccess) | ||
| .map((r) => r.project); | ||
| hasLoaded = true; | ||
| } catch (e) { | ||
| error = e instanceof Error ? e.message : "Failed to load projects"; | ||
|
|
@@ -59,44 +69,112 @@ | |
| } | ||
| } | ||
|
|
||
| async function handleAddProject(projectName: string) { | ||
| isAddingProject = true; | ||
| try { | ||
| await $addProjectMemberUsergroup.mutateAsync({ | ||
| org: organization, | ||
| project: projectName, | ||
| usergroup: groupName, | ||
| data: { | ||
| role: ProjectUserRoles.Viewer, | ||
| }, | ||
| }); | ||
|
|
||
| await queryClient.invalidateQueries({ | ||
| queryKey: getAdminServiceListProjectMemberUsergroupsQueryKey( | ||
| organization, | ||
| projectName, | ||
| ), | ||
| }); | ||
|
|
||
| eventBus.emit("notification", { | ||
| type: "success", | ||
| message: `Added group "${groupName}" to project "${projectName}"`, | ||
| }); | ||
|
|
||
| hasLoaded = false; | ||
| await loadProjectsForGroup(); | ||
| } catch (e) { | ||
| eventBus.emit("notification", { | ||
| type: "error", | ||
| message: `Failed to add group to project: ${e instanceof Error ? e.message : "Unknown error"}`, | ||
| }); | ||
| } finally { | ||
| isAddingProject = false; | ||
| isAddProjectDropdownOpen = false; | ||
| } | ||
| } | ||
|
|
||
| onMount(() => { | ||
| void loadProjectsForGroup(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }); | ||
|
|
||
| $: projectCount = accessibleProjects.length; | ||
| $: hasProjects = projectCount > 0; | ||
| $: availableProjectsToAdd = allProjects.filter( | ||
| (p) => !accessibleProjects.some((ap) => ap.id === p.id), | ||
| ); | ||
|
|
||
| function getProjectShareUrl(projectName: string | undefined) { | ||
| return `/${organization}/${projectName}/-/dashboards?share=true`; | ||
| } | ||
| </script> | ||
|
|
||
| {#if hasLoaded && hasProjects} | ||
| <Dropdown.Root bind:open={isDropdownOpen}> | ||
| <Dropdown.Trigger | ||
| class="w-18 flex flex-row gap-1 items-center rounded-sm {isDropdownOpen | ||
| ? 'bg-gray-200' | ||
| : 'hover:bg-surface-hover'} px-2 py-1" | ||
| > | ||
| <span class="capitalize"> | ||
| {projectCount} Project{projectCount !== 1 ? "s" : ""} | ||
| </span> | ||
| {#if isDropdownOpen} | ||
| <CaretUpIcon size="12px" /> | ||
| {:else} | ||
| <CaretDownIcon size="12px" /> | ||
| {/if} | ||
| </Dropdown.Trigger> | ||
| <Dropdown.Content align="start"> | ||
| {#each accessibleProjects as project (project.id)} | ||
| <Dropdown.Item href={getProjectShareUrl(project.name)}> | ||
| {project.name} | ||
| </Dropdown.Item> | ||
| {/each} | ||
| </Dropdown.Content> | ||
| </Dropdown.Root> | ||
| {:else if isPending} | ||
| <div class="w-18 rounded-sm px-2 py-1 text-fg-secondary">Loading...</div> | ||
| {:else} | ||
| <div class="w-18 rounded-sm px-2 py-1 text-fg-secondary">No projects</div> | ||
| {/if} | ||
| <div class="flex items-center gap-1"> | ||
| {#if hasLoaded && hasProjects} | ||
| <Dropdown.Root bind:open={isDropdownOpen}> | ||
| <Dropdown.Trigger | ||
| class="flex flex-row gap-1 items-center rounded-sm {isDropdownOpen | ||
| ? 'bg-gray-200' | ||
| : 'hover:bg-surface-hover'} px-2 py-1" | ||
| > | ||
| <span class="capitalize"> | ||
| {projectCount} Project{projectCount !== 1 ? "s" : ""} | ||
| </span> | ||
| {#if isDropdownOpen} | ||
| <CaretUpIcon size="12px" /> | ||
| {:else} | ||
| <CaretDownIcon size="12px" /> | ||
| {/if} | ||
| </Dropdown.Trigger> | ||
| <Dropdown.Content align="start"> | ||
| {#each accessibleProjects as project (project.id)} | ||
| <Dropdown.Item href={getProjectShareUrl(project.name)}> | ||
| {project.name} | ||
| </Dropdown.Item> | ||
| {/each} | ||
| </Dropdown.Content> | ||
| </Dropdown.Root> | ||
| {:else if isPending} | ||
| <div class="rounded-sm px-2 py-1 text-fg-secondary">Loading...</div> | ||
| {:else} | ||
| <div class="rounded-sm px-2 py-1 text-fg-secondary">No projects</div> | ||
| {/if} | ||
|
|
||
| {#if hasLoaded && availableProjectsToAdd.length > 0} | ||
| <Dropdown.Root bind:open={isAddProjectDropdownOpen}> | ||
| <Dropdown.Trigger | ||
| class="flex items-center justify-center rounded-sm {isAddProjectDropdownOpen | ||
| ? 'bg-gray-200' | ||
| : 'hover:bg-surface-hover'} p-1" | ||
| disabled={isAddingProject} | ||
| > | ||
| <Plus size="14px" class="text-fg-secondary" /> | ||
| </Dropdown.Trigger> | ||
| <Dropdown.Content align="start" class="max-h-60 overflow-y-auto"> | ||
| <div class="px-2 py-1 text-xs text-fg-secondary font-medium"> | ||
| Add to project | ||
| </div> | ||
| {#each availableProjectsToAdd as project (project.id)} | ||
| <Dropdown.Item | ||
| on:click={() => handleAddProject(project.name ?? "")} | ||
| disabled={isAddingProject} | ||
| > | ||
| {project.name} | ||
| </Dropdown.Item> | ||
| {/each} | ||
| </Dropdown.Content> | ||
| </Dropdown.Root> | ||
| {/if} | ||
| </div> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use a tanstack query. This direct call will not cache the results.