Skip to content

Commit 142f17d

Browse files
committed
feat: revamp token creation flow
1 parent cee1c27 commit 142f17d

File tree

2 files changed

+190
-37
lines changed

2 files changed

+190
-37
lines changed

src/components/Tokens/Add/CreateTokenForm.tsx

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ import { Button } from "../../../ui/Buttons/Button";
1212
import { Modal } from "../../../ui/Modal";
1313
import FormikTextInput from "../../Forms/Formik/FormikTextInput";
1414
import FormikSelectDropdown from "../../Forms/Formik/FormikSelectDropdown";
15-
import FormikCheckbox from "../../Forms/Formik/FormikCheckbox";
1615
import { toastError, toastSuccess } from "../../Toast/toast";
17-
import {
18-
OBJECTS,
19-
getActionsForObject,
20-
getAllObjectActions
21-
} from "../tokenUtils";
16+
import { getAllObjectActions } from "../tokenUtils";
17+
import TokenScopeFieldsGroup from "./TokenScopeFieldsGroup";
2218

2319
export type TokenFormValues = CreateTokenRequest & {
2420
objectActions: Record<string, boolean>;
@@ -139,37 +135,7 @@ export default function CreateTokenForm({
139135
hint="When this token should expire"
140136
/>
141137

142-
<div className="space-y-3">
143-
<div className="text-sm font-medium text-gray-700">
144-
Scopes
145-
<p className="mt-1 text-xs text-gray-500">
146-
Select the permissions to grant to this token
147-
</p>
148-
</div>
149-
<div className="max-h-64 space-y-4 overflow-y-auto rounded-md border bg-gray-50 p-4">
150-
{OBJECTS.map((object) => (
151-
<div key={object} className="space-y-2">
152-
<div className="text-sm font-medium text-gray-800">
153-
{object}
154-
</div>
155-
<div className="grid grid-cols-4 gap-2 pl-4">
156-
{getActionsForObject(object).map((action) => {
157-
const scopeKey = `${object}:${action}`;
158-
return (
159-
<FormikCheckbox
160-
key={scopeKey}
161-
name={`objectActions.${scopeKey}`}
162-
label={action}
163-
labelClassName="text-sm font-normal text-gray-600"
164-
inline={true}
165-
/>
166-
);
167-
})}
168-
</div>
169-
</div>
170-
))}
171-
</div>
172-
</div>
138+
<TokenScopeFieldsGroup isMcpSetup={isMcpSetup} />
173139
</div>
174140
</div>
175141
</div>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { useFormikContext } from "formik";
2+
import { useCallback, useEffect, useState, useMemo } from "react";
3+
import { Switch } from "../../../ui/FormControls/Switch";
4+
import FormikCheckbox from "../../Forms/Formik/FormikCheckbox";
5+
import { OBJECTS, getActionsForObject } from "../tokenUtils";
6+
import { TokenFormValues } from "./CreateTokenForm";
7+
8+
const ScopeOptions = ["Read", "Write", "Admin", "Custom"] as const;
9+
type ScopeType = (typeof ScopeOptions)[number];
10+
11+
// Pre-calculate scope mappings outside component to avoid recalculation
12+
const SCOPE_MAPPINGS = {
13+
Read: (() => {
14+
const scopes: Record<string, boolean> = {};
15+
OBJECTS.forEach((object) => {
16+
const actions = getActionsForObject(object);
17+
if (actions.includes("read")) {
18+
scopes[`${object}:read`] = true;
19+
} else if (object === "mcp" && actions.includes("*")) {
20+
scopes[`${object}:*`] = true;
21+
}
22+
});
23+
return scopes;
24+
})(),
25+
Write: (() => {
26+
const scopes: Record<string, boolean> = {};
27+
OBJECTS.forEach((object) => {
28+
const actions = getActionsForObject(object);
29+
actions.forEach((action) => {
30+
if (
31+
["read", "create"].includes(action) ||
32+
(object === "mcp" && action === "*")
33+
) {
34+
scopes[`${object}:${action}`] = true;
35+
}
36+
});
37+
});
38+
return scopes;
39+
})(),
40+
Admin: (() => {
41+
const scopes: Record<string, boolean> = {};
42+
OBJECTS.forEach((object) => {
43+
const actions = getActionsForObject(object);
44+
actions.forEach((action) => {
45+
if (object === "playbooks") {
46+
scopes[`${object}:${action}`] = true;
47+
} else if (object === "mcp" && action === "*") {
48+
scopes[`${object}:${action}`] = true;
49+
} else if (!action.startsWith("playbook:")) {
50+
scopes[`${object}:${action}`] = true;
51+
}
52+
});
53+
});
54+
return scopes;
55+
})()
56+
};
57+
58+
// Pre-calculate object actions to avoid function calls in render
59+
const OBJECT_ACTIONS = OBJECTS.reduce(
60+
(acc, object) => {
61+
acc[object] = getActionsForObject(object);
62+
return acc;
63+
},
64+
{} as Record<string, string[]>
65+
);
66+
67+
type TokenScopeFieldsGroupProps = {
68+
isMcpSetup?: boolean;
69+
};
70+
71+
export default function TokenScopeFieldsGroup({
72+
isMcpSetup = false
73+
}: TokenScopeFieldsGroupProps) {
74+
const { setFieldValue, values } = useFormikContext<TokenFormValues>();
75+
76+
const [selectedScope, setSelectedScope] = useState<ScopeType>(() => {
77+
if (isMcpSetup) {
78+
return "Custom";
79+
}
80+
return "Read";
81+
});
82+
83+
// Memoize the keys from objectActions to avoid dependency on the whole object
84+
const objectActionKeys = useMemo(() => {
85+
return Object.keys(values.objectActions);
86+
}, [values.objectActions]);
87+
88+
const applyScopePreset = useCallback(
89+
(scope: ScopeType) => {
90+
let newObjectActions: Record<string, boolean> = {};
91+
92+
// Reset all scopes first
93+
objectActionKeys.forEach((key) => {
94+
newObjectActions[key] = false;
95+
});
96+
97+
if (scope !== "Custom") {
98+
// Use pre-calculated scope mappings
99+
newObjectActions = { ...newObjectActions, ...SCOPE_MAPPINGS[scope] };
100+
} else if (isMcpSetup) {
101+
// Pre-select MCP * action for MCP setup
102+
newObjectActions["mcp:*"] = true;
103+
}
104+
105+
setFieldValue("objectActions", newObjectActions);
106+
},
107+
[objectActionKeys, setFieldValue, isMcpSetup]
108+
);
109+
110+
useEffect(() => {
111+
applyScopePreset(selectedScope);
112+
}, [selectedScope, applyScopePreset]);
113+
114+
const handleScopeChange = useCallback((scope: string) => {
115+
setSelectedScope(scope as ScopeType);
116+
}, []);
117+
118+
return (
119+
<div className="space-y-3">
120+
<div className="text-sm font-medium text-gray-700">
121+
Scopes
122+
<p className="mt-1 text-xs text-gray-500">
123+
Select the permissions to grant to this token
124+
</p>
125+
</div>
126+
127+
<div className="flex flex-col space-y-2">
128+
<label className="text-sm font-semibold">Permission Level</label>
129+
<div className="flex w-full flex-row">
130+
<Switch
131+
options={ScopeOptions as unknown as string[]}
132+
defaultValue={isMcpSetup ? "Custom" : "Read"}
133+
value={selectedScope}
134+
onChange={handleScopeChange}
135+
/>
136+
</div>
137+
</div>
138+
139+
{selectedScope === "Custom" && (
140+
<div className="max-h-64 space-y-4 overflow-y-auto rounded-md border bg-gray-50 p-4">
141+
{OBJECTS.map((object) => (
142+
<div key={object} className="space-y-2">
143+
<div className="text-sm font-medium text-gray-800">{object}</div>
144+
<div className="grid grid-cols-4 gap-2 pl-4">
145+
{OBJECT_ACTIONS[object].map((action) => {
146+
const scopeKey = `${object}:${action}`;
147+
return (
148+
<FormikCheckbox
149+
key={scopeKey}
150+
name={`objectActions.${scopeKey}`}
151+
label={action}
152+
labelClassName="text-sm font-normal text-gray-600"
153+
inline={true}
154+
/>
155+
);
156+
})}
157+
</div>
158+
</div>
159+
))}
160+
</div>
161+
)}
162+
163+
{selectedScope !== "Custom" && (
164+
<div className="rounded-md border bg-blue-50 p-3">
165+
<div className="text-sm text-blue-800">
166+
<strong>{selectedScope}</strong> permissions selected:
167+
<ul className="mt-1 list-inside list-disc text-xs">
168+
{selectedScope === "Read" && <li>Read access to all objects</li>}
169+
{selectedScope === "Write" && (
170+
<>
171+
<li>Read access to all objects</li>
172+
<li>Create access to all objects</li>
173+
</>
174+
)}
175+
{selectedScope === "Admin" && (
176+
<>
177+
<li>Full CRUD access to all objects</li>
178+
<li>Playbook execution permissions</li>
179+
</>
180+
)}
181+
</ul>
182+
</div>
183+
</div>
184+
)}
185+
</div>
186+
);
187+
}

0 commit comments

Comments
 (0)