Skip to content

Commit 82241df

Browse files
authored
feat(workspaces): implement workspace roles (#305)
1 parent ff37d66 commit 82241df

File tree

13 files changed

+602
-94
lines changed

13 files changed

+602
-94
lines changed

console/src/components/common/layout/settings-layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ export default function SettingsLayout({
6363
{/* Back To App button - always visible on the left */}
6464
<Link to="/" className="mr-auto">
6565
<Button variant="outline" className="hidden md:flex">
66-
<ArrowLeft className="mr-2 h-4 w-4" />
67-
Back To App
66+
<ArrowLeft className="h-4 w-4" />
67+
Back
6868
</Button>
6969
<Button
7070
variant="outline"
7171
size="icon"
7272
className="md:hidden"
73-
title="Back To App"
73+
title="Back"
7474
>
7575
<ArrowLeft className="h-4 w-4" />
7676
</Button>

console/src/pages/settings/components/api-keys-settings.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { Button } from '@/components/ui/button';
2-
import {
3-
Card,
4-
CardContent,
5-
CardDescription,
6-
CardHeader,
7-
CardTitle,
8-
} from '@/components/ui/card';
2+
import { Card, CardContent } from '@/components/ui/card';
93
import { useWorkspaceSelector } from '@/hooks/useWorkspaceSelector';
104
import {
115
useWorkspacesControllerGetWorkspaceApiKey,
@@ -78,12 +72,6 @@ export default function ApiKeysSettings() {
7872
return (
7973
<div className="space-y-6">
8074
<Card>
81-
<CardHeader>
82-
<CardTitle>Workspace API Key</CardTitle>
83-
<CardDescription>
84-
Use this API key to authenticate API requests for this workspace
85-
</CardDescription>
86-
</CardHeader>
8775
<CardContent className="space-y-4">
8876
{isLoading ? (
8977
<div className="flex items-center justify-center py-4">
@@ -95,29 +83,27 @@ export default function ApiKeysSettings() {
9583
) : (
9684
<>
9785
<div className="flex min-w-0 flex-col gap-2">
98-
<div className="overflow-x-auto">
99-
<code className="whitespace-nowrap rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
100-
{apiKeyData?.apiKey || 'No API key available'}
101-
</code>
86+
<div className="overflow-x-auto h-10 flex justify-center items-center rounded border">
87+
<code>{apiKeyData?.apiKey || 'No API key available'}</code>
10288
</div>
103-
<div className="flex gap-2">
89+
<div className="flex gap-2 justify-center">
10490
<Button
10591
variant="outline"
10692
size="sm"
10793
onClick={handleCopy}
10894
disabled={!apiKeyData?.apiKey}
10995
>
110-
<Copy className="mr-2 h-4 w-4" />
96+
<Copy className="h-4 w-4" />
11197
{copied ? 'Copied!' : 'Copy'}
11298
</Button>
11399
<Button
114-
variant="destructive"
100+
variant="secondary"
115101
size="sm"
116102
onClick={handleRotate}
117103
disabled={isRotating || !selectedWorkspace}
118104
>
119105
<RefreshCw
120-
className={`mr-2 h-4 w-4 ${isRotating ? 'animate-spin' : ''}`}
106+
className={`h-4 w-4 ${isRotating ? 'animate-spin' : ''}`}
121107
/>
122108
{isRotating ? 'Rotating...' : 'Rotate'}
123109
</Button>

console/src/pages/settings/components/workspace-settings.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog';
1010
import {
1111
Form,
1212
FormControl,
13-
FormDescription,
1413
FormField,
1514
FormItem,
1615
FormLabel,
@@ -135,9 +134,6 @@ export default function WorkspaceSettings() {
135134
<Card>
136135
<CardHeader>
137136
<CardTitle>Workspace Information</CardTitle>
138-
<CardDescription>
139-
Manage your workspace name and description
140-
</CardDescription>
141137
</CardHeader>
142138
<CardContent>
143139
<Form {...form}>
@@ -150,13 +146,10 @@ export default function WorkspaceSettings() {
150146
name="name"
151147
render={({ field }) => (
152148
<FormItem>
153-
<FormLabel>Workspace Name</FormLabel>
149+
<FormLabel>Name</FormLabel>
154150
<FormControl>
155-
<Input placeholder="Workspace name" {...field} />
151+
<Input {...field} />
156152
</FormControl>
157-
<FormDescription>
158-
This is the display name of your workspace
159-
</FormDescription>
160153
<FormMessage />
161154
</FormItem>
162155
)}
@@ -168,7 +161,7 @@ export default function WorkspaceSettings() {
168161
<FormItem>
169162
<FormLabel>Description</FormLabel>
170163
<FormControl>
171-
<Input placeholder="Workspace description" {...field} />
164+
<Input {...field} />
172165
</FormControl>
173166
<FormMessage />
174167
</FormItem>

console/src/pages/settings/settings.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { useSession } from '@/utils/authClient';
12
import { useEffect, useMemo, type JSX } from 'react';
23
import { useNavigate, useParams } from 'react-router-dom';
3-
import { useSession } from '@/utils/authClient';
44
import ApiKeysSettings from './components/api-keys-settings';
55
import BrandNameAndLogoSettings from './components/brand-name-and-logo';
66
import CreateMcpPermission from './components/create-mcp-permission';
@@ -38,12 +38,12 @@ interface SettingsProps {
3838
// Settings tab groups with content and component - exported for SettingsLayout
3939
export const settingsTabGroups: SettingsTabGroup[] = [
4040
{
41-
name: 'Configuration',
41+
name: 'Workspace',
4242
tabs: [
4343
{
44-
id: 'workspace',
45-
label: 'Workspace',
46-
path: '/settings/workspace',
44+
id: 'general',
45+
label: 'General',
46+
path: '/settings/general',
4747
content: {
4848
title: 'Workspace settings',
4949
description: 'Manage your workspace settings',
@@ -52,7 +52,7 @@ export const settingsTabGroups: SettingsTabGroup[] = [
5252
},
5353
{
5454
id: 'apikeys',
55-
label: 'API Keys',
55+
label: 'API keys',
5656
path: '/settings/apikeys',
5757
content: {
5858
title: 'API Keys',
@@ -150,7 +150,7 @@ export function filterTabGroups(
150150
// Backward compatibility - flattened array of all tabs
151151
export const settingsTabs = settingsTabGroups.flatMap((group) => group.tabs);
152152

153-
const Settings = ({ defaultTab = 'workspace' }: SettingsProps) => {
153+
const Settings = ({ defaultTab = 'general' }: SettingsProps) => {
154154
const { tab } = useParams<{ tab?: string }>();
155155
const navigate = useNavigate();
156156
const { data } = useSession();

console/src/pages/workspaces/index.tsx

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ import {
88
CardHeader,
99
CardTitle,
1010
} from '@/components/ui/card';
11+
import {
12+
Tooltip,
13+
TooltipContent,
14+
TooltipTrigger,
15+
} from '@/components/ui/tooltip';
1116
import { useWorkspaceSelector } from '@/hooks/useWorkspaceSelector';
1217
import {
1318
useWorkspacesControllerGetWorkspaces,
19+
WorkspaceResponseDtoRole,
1420
type WorkspaceResponseDto,
1521
} from '@/services/apis/gen/queries';
16-
import { Plus, Target, Users } from 'lucide-react';
22+
import { Crown, Plus, Target, Users } from 'lucide-react';
1723
import { useNavigate } from 'react-router-dom';
1824

1925
const PAGE_SIZE = 12;
@@ -54,7 +60,7 @@ export default function Workspaces() {
5460
}
5561
>
5662
<div className="space-y-4">
57-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
63+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
5864
{isLoading
5965
? Array.from({ length: PAGE_SIZE }).map((_, index) => (
6066
<Card key={index} className="animate-pulse">
@@ -67,53 +73,67 @@ export default function Workspaces() {
6773
</CardContent>
6874
</Card>
6975
))
70-
: data?.data.map((workspace: WorkspaceResponseDto) => (
71-
<Card
72-
key={workspace.id}
73-
className="cursor-pointer transition-colors hover:bg-accent/50"
74-
onClick={() => onSelectWorkspace(workspace.id)}
75-
>
76-
<CardHeader>
77-
<div className="flex items-start justify-between">
78-
<CardTitle className="text-lg">
79-
{workspace.name}
80-
</CardTitle>
81-
<Badge
82-
className={
83-
workspace.archivedAt
84-
? 'bg-gray-500 text-white'
85-
: 'bg-green-500 text-white'
86-
}
87-
>
88-
{workspace.archivedAt ? 'Archived' : 'Active'}
89-
</Badge>
90-
</div>
91-
<div className="flex gap-4 pt-2 text-sm text-muted-foreground">
92-
<div className="flex items-center gap-1.5">
93-
<Users size={14} className="text-muted-foreground" />
94-
<span className="font-medium">
95-
{workspace.memberCount}
96-
</span>
97-
<span>members</span>
76+
: data?.data.map((workspace: WorkspaceResponseDto) => {
77+
const isOwner =
78+
workspace.role === WorkspaceResponseDtoRole.owner;
79+
return (
80+
<Card
81+
key={workspace.id}
82+
className="cursor-pointer transition-colors hover:bg-accent/50"
83+
onClick={() => onSelectWorkspace(workspace.id)}
84+
>
85+
<CardHeader>
86+
<div className="flex items-start justify-between">
87+
<CardTitle className="text-lg flex items-center gap-2">
88+
{workspace.name}
89+
{isOwner && (
90+
<Tooltip>
91+
<TooltipTrigger>
92+
<Crown size={20} className="text-yellow-500" />
93+
</TooltipTrigger>
94+
<TooltipContent>
95+
<p>Owner</p>
96+
</TooltipContent>
97+
</Tooltip>
98+
)}
99+
</CardTitle>
100+
<Badge
101+
className={
102+
workspace.archivedAt
103+
? 'bg-gray-500 text-white'
104+
: 'bg-green-500 text-white'
105+
}
106+
>
107+
{workspace.archivedAt ? 'Archived' : 'Active'}
108+
</Badge>
98109
</div>
99-
<div className="flex items-center gap-1.5">
100-
<Target size={14} className="text-muted-foreground" />
101-
<span className="font-medium">
102-
{workspace.targetCount}
103-
</span>
104-
<span>targets</span>
110+
<div className="flex gap-4 pt-2 text-sm text-muted-foreground">
111+
<div className="flex items-center gap-1.5">
112+
<Users size={14} className="text-muted-foreground" />
113+
<span className="font-medium">
114+
{workspace.memberCount}
115+
</span>
116+
<span>members</span>
117+
</div>
118+
<div className="flex items-center gap-1.5">
119+
<Target size={14} className="text-muted-foreground" />
120+
<span className="font-medium">
121+
{workspace.targetCount}
122+
</span>
123+
<span>targets</span>
124+
</div>
105125
</div>
106-
</div>
107-
</CardHeader>
108-
<CardContent>
109-
<CardDescription className="line-clamp-2">
110-
{typeof workspace.description === 'string'
111-
? workspace.description
112-
: 'No description'}
113-
</CardDescription>
114-
</CardContent>
115-
</Card>
116-
))}
126+
</CardHeader>
127+
<CardContent>
128+
<CardDescription className="line-clamp-2">
129+
{typeof workspace.description === 'string'
130+
? workspace.description
131+
: 'No description'}
132+
</CardDescription>
133+
</CardContent>
134+
</Card>
135+
);
136+
})}
117137
</div>
118138
</div>
119139
</Page>

console/src/services/apis/gen/queries.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,18 @@ export type WorkspaceResponseDtoDescription = { [key: string]: unknown } | null;
218218
*/
219219
export type WorkspaceResponseDtoArchivedAt = { [key: string]: unknown } | null;
220220

221+
/**
222+
* Role of the current user in the workspace
223+
*/
224+
export type WorkspaceResponseDtoRole =
225+
(typeof WorkspaceResponseDtoRole)[keyof typeof WorkspaceResponseDtoRole];
226+
227+
// eslint-disable-next-line @typescript-eslint/no-redeclare
228+
export const WorkspaceResponseDtoRole = {
229+
owner: 'owner',
230+
member: 'member',
231+
} as const;
232+
221233
export type WorkspaceResponseDto = {
222234
/** Workspace ID */
223235
id: string;
@@ -247,6 +259,8 @@ export type WorkspaceResponseDto = {
247259
targetCount: number;
248260
/** Number of members in the workspace */
249261
memberCount: number;
262+
/** Role of the current user in the workspace */
263+
role: WorkspaceResponseDtoRole;
250264
};
251265

252266
export type GetManyWorkspaceResponseDtoDto = {
@@ -15149,6 +15163,7 @@ export const useToolsControllerUninstallTool = <
1514915163
};
1515015164

1515115165
/**
15166+
* @deprecated
1515215167
* @summary Get built-in tools
1515315168
*/
1515415169
export const toolsControllerGetBuiltInTools = (
@@ -15270,6 +15285,7 @@ export function useToolsControllerGetBuiltInTools<
1527015285
queryKey: DataTag<QueryKey, TData, TError>;
1527115286
};
1527215287
/**
15288+
* @deprecated
1527315289
* @summary Get built-in tools
1527415290
*/
1527515291

core-api/src/common/enums/enum.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export enum Role {
1010
BOT = 'bot',
1111
}
1212

13+
export enum WorkspaceRole {
14+
OWNER = 'owner',
15+
MEMBER = 'member',
16+
}
17+
1318
/**
1419
* Enum representing tool categories used in the system
1520
*/
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class WorkspaceRole1773890090920 implements MigrationInterface {
4+
name = 'WorkspaceRole1773890090920'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TYPE "public"."workspace_members_role_enum" AS ENUM('owner', 'member')`);
8+
await queryRunner.query(`ALTER TABLE "workspace_members" ADD "role" "public"."workspace_members_role_enum" NOT NULL DEFAULT 'owner'`);
9+
}
10+
11+
public async down(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(`ALTER TABLE "workspace_members" DROP COLUMN "role"`);
13+
await queryRunner.query(`DROP TYPE "public"."workspace_members_role_enum"`);
14+
}
15+
16+
}

0 commit comments

Comments
 (0)