Skip to content

Commit a8a756f

Browse files
committed
feat(rbac): ui consistency
1 parent 3e61bd4 commit a8a756f

File tree

9 files changed

+453
-59
lines changed

9 files changed

+453
-59
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client"
2+
3+
import { ScopeGuard } from "@/components/auth/scope-guard"
4+
import { CenteredSpinner } from "@/components/loading/spinner"
5+
import { AlertNotification } from "@/components/notifications"
6+
import { WorkspaceRbacGroups } from "@/components/workspaces/workspace-rbac-groups"
7+
import { useWorkspaceDetails } from "@/hooks/use-workspace"
8+
9+
export default function WorkspaceGroupsPage() {
10+
const { workspace, workspaceLoading, workspaceError } = useWorkspaceDetails()
11+
if (workspaceLoading) {
12+
return <CenteredSpinner />
13+
}
14+
if (workspaceError) {
15+
return (
16+
<AlertNotification
17+
level="error"
18+
message="Error loading workspace info."
19+
/>
20+
)
21+
}
22+
if (!workspace) {
23+
return <AlertNotification level="error" message="Workspace not found." />
24+
}
25+
return (
26+
<ScopeGuard scope="workspace:rbac:read" fallback={null} loading={null}>
27+
<div className="size-full overflow-auto">
28+
<div className="container flex h-full max-w-[1200px] flex-col space-y-8 py-6">
29+
<div className="flex w-full">
30+
<div className="items-start space-y-3 text-left">
31+
<h2 className="text-2xl font-semibold tracking-tight">Groups</h2>
32+
<p className="text-md text-muted-foreground">
33+
Manage workspace groups and their members.
34+
</p>
35+
</div>
36+
</div>
37+
<WorkspaceRbacGroups workspaceId={workspace.id} hideCreateButton />
38+
</div>
39+
</div>
40+
</ScopeGuard>
41+
)
42+
}

frontend/src/app/workspaces/[workspaceId]/members/page.tsx

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
"use client"
22

3-
import { ScopeGuard } from "@/components/auth/scope-guard"
43
import { CenteredSpinner } from "@/components/loading/spinner"
54
import { AlertNotification } from "@/components/notifications"
6-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
75
import { WorkspaceMembersTable } from "@/components/workspaces/workspace-members-table"
8-
import { WorkspaceRbacGroups } from "@/components/workspaces/workspace-rbac-groups"
9-
import { WorkspaceRbacRoles } from "@/components/workspaces/workspace-rbac-roles"
106
import { useWorkspaceDetails } from "@/hooks/use-workspace"
117

12-
const tabTriggerClassName =
13-
"rounded-none border-b-2 border-transparent px-4 py-2.5 data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
14-
158
export default function WorkspaceMembersPage() {
169
const { workspace, workspaceLoading, workspaceError } = useWorkspaceDetails()
1710
if (workspaceLoading) {
@@ -39,40 +32,7 @@ export default function WorkspaceMembersPage() {
3932
</p>
4033
</div>
4134
</div>
42-
<Tabs defaultValue="members" className="w-full">
43-
<TabsList className="inline-flex h-auto w-auto justify-start gap-0 rounded-none border-b border-border/30 bg-transparent p-0">
44-
<TabsTrigger value="members" className={tabTriggerClassName}>
45-
Members
46-
</TabsTrigger>
47-
<ScopeGuard
48-
scope="workspace:rbac:read"
49-
fallback={null}
50-
loading={null}
51-
>
52-
<TabsTrigger value="roles" className={tabTriggerClassName}>
53-
Roles
54-
</TabsTrigger>
55-
<TabsTrigger value="groups" className={tabTriggerClassName}>
56-
Groups
57-
</TabsTrigger>
58-
</ScopeGuard>
59-
</TabsList>
60-
<TabsContent value="members" className="mt-6">
61-
<WorkspaceMembersTable workspace={workspace} />
62-
</TabsContent>
63-
<ScopeGuard
64-
scope="workspace:rbac:read"
65-
fallback={null}
66-
loading={null}
67-
>
68-
<TabsContent value="roles" className="mt-6">
69-
<WorkspaceRbacRoles workspaceId={workspace.id} />
70-
</TabsContent>
71-
<TabsContent value="groups" className="mt-6">
72-
<WorkspaceRbacGroups workspaceId={workspace.id} />
73-
</TabsContent>
74-
</ScopeGuard>
75-
</Tabs>
35+
<WorkspaceMembersTable workspace={workspace} />
7636
</div>
7737
</div>
7838
)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client"
2+
3+
import { ScopeGuard } from "@/components/auth/scope-guard"
4+
import { CenteredSpinner } from "@/components/loading/spinner"
5+
import { AlertNotification } from "@/components/notifications"
6+
import { WorkspaceRbacRoles } from "@/components/workspaces/workspace-rbac-roles"
7+
import { useWorkspaceDetails } from "@/hooks/use-workspace"
8+
9+
export default function WorkspaceRolesPage() {
10+
const { workspace, workspaceLoading, workspaceError } = useWorkspaceDetails()
11+
if (workspaceLoading) {
12+
return <CenteredSpinner />
13+
}
14+
if (workspaceError) {
15+
return (
16+
<AlertNotification
17+
level="error"
18+
message="Error loading workspace info."
19+
/>
20+
)
21+
}
22+
if (!workspace) {
23+
return <AlertNotification level="error" message="Workspace not found." />
24+
}
25+
return (
26+
<ScopeGuard scope="workspace:rbac:read" fallback={null} loading={null}>
27+
<div className="size-full overflow-auto">
28+
<div className="container flex h-full max-w-[1200px] flex-col space-y-8 py-6">
29+
<div className="flex w-full">
30+
<div className="items-start space-y-3 text-left">
31+
<h2 className="text-2xl font-semibold tracking-tight">Roles</h2>
32+
<p className="text-md text-muted-foreground">
33+
Manage workspace roles and permissions.
34+
</p>
35+
</div>
36+
</div>
37+
<WorkspaceRbacRoles workspaceId={workspace.id} hideCreateButton />
38+
</div>
39+
</div>
40+
</ScopeGuard>
41+
)
42+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"use client"
2+
3+
import { Layers, Shield, Users } from "lucide-react"
4+
import Link from "next/link"
5+
import { ScopeGuard } from "@/components/auth/scope-guard"
6+
import {
7+
Tooltip,
8+
TooltipContent,
9+
TooltipProvider,
10+
TooltipTrigger,
11+
} from "@/components/ui/tooltip"
12+
import { cn } from "@/lib/utils"
13+
14+
export enum MembersViewMode {
15+
Members = "members",
16+
Roles = "roles",
17+
Groups = "groups",
18+
}
19+
20+
interface MembersViewToggleProps {
21+
view: MembersViewMode
22+
className?: string
23+
membersHref: string
24+
rolesHref: string
25+
groupsHref: string
26+
/** The scope required to view roles and groups (e.g., "workspace:rbac:read" or "org:rbac:read") */
27+
rbacScope: string
28+
}
29+
30+
export function MembersViewToggle({
31+
view,
32+
className,
33+
membersHref,
34+
rolesHref,
35+
groupsHref,
36+
rbacScope,
37+
}: MembersViewToggleProps) {
38+
const toggleItems = [
39+
{
40+
mode: MembersViewMode.Members,
41+
icon: Users,
42+
tooltip: "Members",
43+
href: membersHref,
44+
ariaLabel: "Members view",
45+
requiresScope: false,
46+
},
47+
{
48+
mode: MembersViewMode.Roles,
49+
icon: Shield,
50+
tooltip: "Roles",
51+
href: rolesHref,
52+
ariaLabel: "Roles view",
53+
requiresScope: true,
54+
},
55+
{
56+
mode: MembersViewMode.Groups,
57+
icon: Layers,
58+
tooltip: "Groups",
59+
href: groupsHref,
60+
ariaLabel: "Groups view",
61+
requiresScope: true,
62+
},
63+
] as const
64+
65+
return (
66+
<div
67+
className={cn(
68+
"inline-flex items-center rounded-md border bg-transparent",
69+
className
70+
)}
71+
>
72+
<TooltipProvider>
73+
{toggleItems.map((item, index) => {
74+
const Icon = item.icon
75+
const isActive = view === item.mode
76+
const isFirst = index === 0
77+
const isLast = index === toggleItems.length - 1
78+
const roundedClass = cn({
79+
"rounded-l-sm": isFirst,
80+
"rounded-none": !isFirst && !isLast,
81+
"rounded-r-sm": isLast,
82+
})
83+
const baseClasses = cn(
84+
"flex size-7 items-center justify-center transition-colors",
85+
roundedClass,
86+
isActive
87+
? "bg-background text-accent-foreground"
88+
: "bg-accent text-muted-foreground hover:bg-muted/50"
89+
)
90+
91+
const content = (
92+
<Tooltip key={item.mode}>
93+
<TooltipTrigger asChild>
94+
<Link
95+
href={item.href}
96+
className={baseClasses}
97+
aria-current={isActive ? "page" : undefined}
98+
aria-label={item.ariaLabel}
99+
>
100+
<Icon className="size-3.5" />
101+
</Link>
102+
</TooltipTrigger>
103+
<TooltipContent>
104+
<p>{item.tooltip}</p>
105+
</TooltipContent>
106+
</Tooltip>
107+
)
108+
109+
if (item.requiresScope) {
110+
return (
111+
<ScopeGuard
112+
key={item.mode}
113+
scope={rbacScope}
114+
fallback={null}
115+
loading={null}
116+
>
117+
{content}
118+
</ScopeGuard>
119+
)
120+
}
121+
122+
return content
123+
})}
124+
</TooltipProvider>
125+
</div>
126+
)
127+
}

frontend/src/components/nav/controls-header.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ import {
4949
import { CreateCustomProviderDialog } from "@/components/integrations/create-custom-provider-dialog"
5050
import { MCPIntegrationDialog } from "@/components/integrations/mcp-integration-dialog"
5151
import { Spinner } from "@/components/loading/spinner"
52+
import {
53+
MembersViewMode,
54+
MembersViewToggle,
55+
} from "@/components/members/members-view-toggle"
56+
import { CreateGroupButton } from "@/components/rbac/create-group-button"
57+
import { CreateRoleButton } from "@/components/rbac/create-role-button"
5258
import { CreateTableDialog } from "@/components/tables/table-create-dialog"
5359
import { TableImportTableDialog } from "@/components/tables/table-import-table-dialog"
5460
import { TableInsertButton } from "@/components/tables/table-insert-button"
@@ -843,14 +849,36 @@ function CasesSelectionActionsBar() {
843849
)
844850
}
845851

846-
function MembersActions() {
852+
function MembersActions({ view }: { view: MembersViewMode }) {
847853
const { workspace } = useWorkspaceDetails()
854+
const workspaceId = useWorkspaceId()
848855

849856
if (!workspace) {
850857
return null
851858
}
852859

853-
return <AddWorkspaceMember workspace={workspace} />
860+
// Render the appropriate action button based on the current view
861+
const actionButton =
862+
view === MembersViewMode.Roles ? (
863+
<CreateRoleButton workspaceOnly />
864+
) : view === MembersViewMode.Groups ? (
865+
<CreateGroupButton />
866+
) : (
867+
<AddWorkspaceMember workspace={workspace} />
868+
)
869+
870+
return (
871+
<>
872+
<MembersViewToggle
873+
view={view}
874+
membersHref={`/workspaces/${workspaceId}/members`}
875+
rolesHref={`/workspaces/${workspaceId}/members/roles`}
876+
groupsHref={`/workspaces/${workspaceId}/members/groups`}
877+
rbacScope="workspace:rbac:read"
878+
/>
879+
{actionButton}
880+
</>
881+
)
854882
}
855883

856884
function CredentialsActions() {
@@ -1186,10 +1214,24 @@ function getPageConfig(
11861214
}
11871215
}
11881216

1189-
if (pagePath.startsWith("/members")) {
1217+
if (pagePath === "/members") {
11901218
return {
11911219
title: "Members",
1192-
actions: <MembersActions />,
1220+
actions: <MembersActions view={MembersViewMode.Members} />,
1221+
}
1222+
}
1223+
1224+
if (pagePath === "/members/roles") {
1225+
return {
1226+
title: "Roles",
1227+
actions: <MembersActions view={MembersViewMode.Roles} />,
1228+
}
1229+
}
1230+
1231+
if (pagePath === "/members/groups") {
1232+
return {
1233+
title: "Groups",
1234+
actions: <MembersActions view={MembersViewMode.Groups} />,
11931235
}
11941236
}
11951237

0 commit comments

Comments
 (0)