@grackle-ai/web-components 0.107.2 → 0.108.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/c8da732c75a50be68d2a40b1c14aeab2654cc1af.tar.log +240 -0
- package/.rush/temp/{3ae72563f781afd72723475938136f113846603e.untar.log → c8da732c75a50be68d2a40b1c14aeab2654cc1af.untar.log} +2 -2
- package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +18 -0
- package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +121 -0
- package/.rush/temp/fb761ea36fead333e4d2b59ce5ade8fb1bc8d724.tar.log +12 -0
- package/.rush/temp/{bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log → fb761ea36fead333e4d2b59ce5ade8fb1bc8d724.untar.log} +2 -2
- package/.rush/temp/operation/_phase_build/all.log +5 -5
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +5 -5
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/operation/_phase_test/all.log +17 -17
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +17 -17
- package/.rush/temp/operation/_phase_test/state.json +1 -1
- package/dist/index.js +7038 -6764
- package/package.json +2 -2
- package/rush-logs/web-components._phase_build.cache.log +1 -1
- package/rush-logs/web-components._phase_build.log +18 -0
- package/rush-logs/web-components._phase_test.cache.log +1 -1
- package/rush-logs/web-components._phase_test.log +121 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +1 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +65 -8
- package/src/components/panels/GitHubAccountsPanel.tsx +223 -0
- package/src/components/panels/index.ts +2 -0
- package/src/components/settings/SettingsNav.stories.tsx +11 -10
- package/src/components/settings/SettingsNav.tsx +2 -1
- package/src/context/GrackleContextTypes.ts +3 -0
- package/src/hooks/types.ts +36 -3
- package/src/index.ts +4 -3
- package/src/mocks/MockGrackleProvider.tsx +21 -0
- package/src/mocks/mockData.ts +5 -0
- package/src/test-utils/storybook-helpers.ts +1 -0
- package/src/utils/breadcrumbs.test.ts +1 -0
- package/src/utils/dashboard.test.ts +1 -0
- package/src/utils/navigation.ts +3 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +810 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grackle-ai/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.108.1",
|
|
4
4
|
"description": "Presentational React component library for the Grackle web UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"remark-gfm": "^4.0.0",
|
|
41
41
|
"lucide-react": "~0.474.0",
|
|
42
42
|
"react-router": "^7.0.0",
|
|
43
|
-
"@grackle-ai/common": "0.
|
|
43
|
+
"@grackle-ai/common": "0.108.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@rushstack/heft": "1.2.7",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Invoking: heft run --only build -- --clean
|
|
2
|
+
---- build started ----
|
|
3
|
+
[build:typescript] Using TypeScript version 5.7.3
|
|
4
|
+
[build:storybook-build] Building Storybook...
|
|
5
|
+
[build:storybook-build] Storybook build completed.
|
|
6
|
+
[build:vite-build] Starting Vite build...
|
|
7
|
+
[build:lint] Using ESLint version 9.39.4
|
|
8
|
+
vite v6.4.1 building for production...
|
|
9
|
+
transforming...
|
|
10
|
+
✓ 2574 modules transformed.
|
|
11
|
+
rendering chunks...
|
|
12
|
+
computing gzip size...
|
|
13
|
+
dist/index.css 156.05 kB │ gzip: 20.36 kB
|
|
14
|
+
dist/index.js 1,373.42 kB │ gzip: 351.63 kB
|
|
15
|
+
✓ built in 6.79s
|
|
16
|
+
[build:vite-build] Vite build completed.
|
|
17
|
+
---- build finished (97.523s) ----
|
|
18
|
+
-------------------- Finished (97.527s) --------------------
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Invoking: heft run --only test
|
|
2
|
+
The provided list of phases does not contain all phase dependencies. You may need to run the excluded phases manually.
|
|
3
|
+
---- test started ----
|
|
4
|
+
[test:vitest] Running vitest...
|
|
5
|
+
|
|
6
|
+
RUN v3.2.4 /home/runner/work/grackle/grackle/packages/web-components
|
|
7
|
+
|
|
8
|
+
✓ src/utils/sessionEvents.test.ts (14 tests) 48ms
|
|
9
|
+
✓ src/utils/eventContent.test.ts (38 tests) 155ms
|
|
10
|
+
✓ src/utils/dashboard.test.ts (4 tests) 33ms
|
|
11
|
+
✓ src/utils/route-config.test.ts (23 tests) 59ms
|
|
12
|
+
✓ src/utils/scrollUtils.test.ts (11 tests) 37ms
|
|
13
|
+
✓ src/utils/breadcrumbs.test.ts (18 tests) 58ms
|
|
14
|
+
✓ src/components/tools/classifyTool.test.ts (6 tests) 12ms
|
|
15
|
+
✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 16ms
|
|
16
|
+
✓ src/components/display/extractText.test.tsx (8 tests) 17ms
|
|
17
|
+
✓ src/components/editable/useEditableField.test.tsx (17 tests) 164ms
|
|
18
|
+
✓ src/hooks/useEventSelection.test.ts (13 tests) 220ms
|
|
19
|
+
✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 136ms
|
|
20
|
+
|
|
21
|
+
Test Files 12 passed (12)
|
|
22
|
+
Tests 166 passed (166)
|
|
23
|
+
Start at 02:28:03
|
|
24
|
+
Duration 14.69s (transform 2.51s, setup 0ms, collect 16.67s, tests 957ms, environment 8.65s, prepare 3.84s)
|
|
25
|
+
|
|
26
|
+
[test:vitest] Vitest completed.
|
|
27
|
+
[test:storybook-test] Starting Storybook static server on port 34673...
|
|
28
|
+
[test:storybook-test] Storybook server ready. Running interaction tests...
|
|
29
|
+
jest-haste-map: duplicate manual mock found: adapter-manager
|
|
30
|
+
The following files share their name; please delete one of them:
|
|
31
|
+
* <rootDir>/packages/server/dist/__mocks__/adapter-manager.js
|
|
32
|
+
* <rootDir>/packages/server/src/__mocks__/adapter-manager.ts
|
|
33
|
+
|
|
34
|
+
jest-haste-map: duplicate manual mock found: auto-reconnect
|
|
35
|
+
The following files share their name; please delete one of them:
|
|
36
|
+
* <rootDir>/packages/server/dist/__mocks__/auto-reconnect.js
|
|
37
|
+
* <rootDir>/packages/server/src/__mocks__/auto-reconnect.ts
|
|
38
|
+
|
|
39
|
+
jest-haste-map: duplicate manual mock found: event-bus
|
|
40
|
+
The following files share their name; please delete one of them:
|
|
41
|
+
* <rootDir>/packages/server/dist/__mocks__/event-bus.js
|
|
42
|
+
* <rootDir>/packages/server/src/__mocks__/event-bus.ts
|
|
43
|
+
|
|
44
|
+
jest-haste-map: duplicate manual mock found: event-processor
|
|
45
|
+
The following files share their name; please delete one of them:
|
|
46
|
+
* <rootDir>/packages/server/dist/__mocks__/event-processor.js
|
|
47
|
+
* <rootDir>/packages/server/src/__mocks__/event-processor.ts
|
|
48
|
+
|
|
49
|
+
jest-haste-map: duplicate manual mock found: github-import
|
|
50
|
+
The following files share their name; please delete one of them:
|
|
51
|
+
* <rootDir>/packages/server/dist/__mocks__/github-import.js
|
|
52
|
+
* <rootDir>/packages/server/src/__mocks__/github-import.ts
|
|
53
|
+
|
|
54
|
+
jest-haste-map: duplicate manual mock found: lifecycle
|
|
55
|
+
The following files share their name; please delete one of them:
|
|
56
|
+
* <rootDir>/packages/server/dist/__mocks__/lifecycle.js
|
|
57
|
+
* <rootDir>/packages/server/src/__mocks__/lifecycle.ts
|
|
58
|
+
|
|
59
|
+
jest-haste-map: duplicate manual mock found: log-writer
|
|
60
|
+
The following files share their name; please delete one of them:
|
|
61
|
+
* <rootDir>/packages/server/dist/__mocks__/log-writer.js
|
|
62
|
+
* <rootDir>/packages/server/src/__mocks__/log-writer.ts
|
|
63
|
+
|
|
64
|
+
jest-haste-map: duplicate manual mock found: logger
|
|
65
|
+
The following files share their name; please delete one of them:
|
|
66
|
+
* <rootDir>/packages/server/dist/__mocks__/logger.js
|
|
67
|
+
* <rootDir>/packages/server/src/__mocks__/logger.ts
|
|
68
|
+
|
|
69
|
+
jest-haste-map: duplicate manual mock found: pipe-delivery
|
|
70
|
+
The following files share their name; please delete one of them:
|
|
71
|
+
* <rootDir>/packages/server/dist/__mocks__/pipe-delivery.js
|
|
72
|
+
* <rootDir>/packages/server/src/__mocks__/pipe-delivery.ts
|
|
73
|
+
|
|
74
|
+
jest-haste-map: duplicate manual mock found: processor-registry
|
|
75
|
+
The following files share their name; please delete one of them:
|
|
76
|
+
* <rootDir>/packages/server/dist/__mocks__/processor-registry.js
|
|
77
|
+
* <rootDir>/packages/server/src/__mocks__/processor-registry.ts
|
|
78
|
+
|
|
79
|
+
jest-haste-map: duplicate manual mock found: reanimate-agent
|
|
80
|
+
The following files share their name; please delete one of them:
|
|
81
|
+
* <rootDir>/packages/server/dist/__mocks__/reanimate-agent.js
|
|
82
|
+
* <rootDir>/packages/server/src/__mocks__/reanimate-agent.ts
|
|
83
|
+
|
|
84
|
+
jest-haste-map: duplicate manual mock found: session-recovery
|
|
85
|
+
The following files share their name; please delete one of them:
|
|
86
|
+
* <rootDir>/packages/server/dist/__mocks__/session-recovery.js
|
|
87
|
+
* <rootDir>/packages/server/src/__mocks__/session-recovery.ts
|
|
88
|
+
|
|
89
|
+
jest-haste-map: duplicate manual mock found: stream-hub
|
|
90
|
+
The following files share their name; please delete one of them:
|
|
91
|
+
* <rootDir>/packages/server/dist/__mocks__/stream-hub.js
|
|
92
|
+
* <rootDir>/packages/server/src/__mocks__/stream-hub.ts
|
|
93
|
+
|
|
94
|
+
jest-haste-map: duplicate manual mock found: stream-registry
|
|
95
|
+
The following files share their name; please delete one of them:
|
|
96
|
+
* <rootDir>/packages/server/dist/__mocks__/stream-registry.js
|
|
97
|
+
* <rootDir>/packages/server/src/__mocks__/stream-registry.ts
|
|
98
|
+
|
|
99
|
+
jest-haste-map: duplicate manual mock found: token-push
|
|
100
|
+
The following files share their name; please delete one of them:
|
|
101
|
+
* <rootDir>/packages/server/dist/__mocks__/token-push.js
|
|
102
|
+
* <rootDir>/packages/server/src/__mocks__/token-push.ts
|
|
103
|
+
|
|
104
|
+
jest-haste-map: duplicate manual mock found: utils/exec
|
|
105
|
+
The following files share their name; please delete one of them:
|
|
106
|
+
* <rootDir>/packages/server/dist/__mocks__/utils/exec.js
|
|
107
|
+
* <rootDir>/packages/server/src/__mocks__/utils/exec.ts
|
|
108
|
+
|
|
109
|
+
jest-haste-map: duplicate manual mock found: utils/format-gh-error
|
|
110
|
+
The following files share their name; please delete one of them:
|
|
111
|
+
* <rootDir>/packages/server/dist/__mocks__/utils/format-gh-error.js
|
|
112
|
+
* <rootDir>/packages/server/src/__mocks__/utils/format-gh-error.ts
|
|
113
|
+
|
|
114
|
+
jest-haste-map: duplicate manual mock found: utils/network
|
|
115
|
+
The following files share their name; please delete one of them:
|
|
116
|
+
* <rootDir>/packages/server/dist/__mocks__/utils/network.js
|
|
117
|
+
* <rootDir>/packages/server/src/__mocks__/utils/network.ts
|
|
118
|
+
|
|
119
|
+
[test:storybook-test] Storybook interaction tests completed.
|
|
120
|
+
---- test finished (113.409s) ----
|
|
121
|
+
-------------------- Finished (113.416s) --------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, type JSX } from "react";
|
|
2
2
|
import type { ToastVariant } from "../../context/ToastContext.js";
|
|
3
|
-
import type { Environment, Codespace } from "../../hooks/types.js";
|
|
3
|
+
import type { Environment, Codespace, GitHubAccountData } from "../../hooks/types.js";
|
|
4
4
|
import { ENVIRONMENTS_URL, environmentUrl, useAppNavigate } from "../../utils/navigation.js";
|
|
5
5
|
import { EditableTextField } from "../editable/EditableTextField.js";
|
|
6
6
|
import styles from "./EnvironmentEditPanel.module.scss";
|
|
@@ -17,12 +17,14 @@ interface Props {
|
|
|
17
17
|
environmentId?: string;
|
|
18
18
|
/** All environments (for lookup in edit mode). */
|
|
19
19
|
environments: Environment[];
|
|
20
|
+
/** All registered GitHub accounts for the account selector. */
|
|
21
|
+
githubAccounts: GitHubAccountData[];
|
|
20
22
|
/** Callback to add a new environment. */
|
|
21
|
-
onAddEnvironment: (displayName: string, adapterType: string, adapterConfig?: Record<string, unknown
|
|
23
|
+
onAddEnvironment: (displayName: string, adapterType: string, adapterConfig?: Record<string, unknown>, githubAccountId?: string) => void;
|
|
22
24
|
/** Callback to update an existing environment. */
|
|
23
|
-
onUpdateEnvironment: (environmentId: string, fields: { displayName?: string; adapterConfig?: Record<string, unknown
|
|
24
|
-
/** Callback to list available codespaces. */
|
|
25
|
-
onListCodespaces: () => void;
|
|
25
|
+
onUpdateEnvironment: (environmentId: string, fields: { displayName?: string; adapterConfig?: Record<string, unknown>; githubAccountId?: string }) => void;
|
|
26
|
+
/** Callback to list available codespaces, optionally filtered by GitHub account. */
|
|
27
|
+
onListCodespaces: (githubAccountId?: string) => void;
|
|
26
28
|
/** Available codespaces. */
|
|
27
29
|
codespaces: Codespace[];
|
|
28
30
|
/** Error from codespace operations. */
|
|
@@ -200,7 +202,7 @@ function CodespacePicker({ codespaceName, onCodespaceNameChange, envName, onEnvN
|
|
|
200
202
|
* - edit: pre-populated form; uses click-to-edit fields that auto-save via
|
|
201
203
|
* updateEnvironment.
|
|
202
204
|
*/
|
|
203
|
-
export function EnvironmentEditPanel({ mode, environmentId, environments, onAddEnvironment, onUpdateEnvironment, onListCodespaces, codespaces, codespaceError, codespaceListError, codespaceCreating, onCreateCodespace, onShowToast }: Props): JSX.Element {
|
|
205
|
+
export function EnvironmentEditPanel({ mode, environmentId, environments, githubAccounts, onAddEnvironment, onUpdateEnvironment, onListCodespaces, codespaces, codespaceError, codespaceListError, codespaceCreating, onCreateCodespace, onShowToast }: Props): JSX.Element {
|
|
204
206
|
const navigate = useAppNavigate();
|
|
205
207
|
|
|
206
208
|
const isEdit = mode === "edit";
|
|
@@ -219,6 +221,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
|
|
|
219
221
|
const [image, setImage] = useState("");
|
|
220
222
|
const [repo, setRepo] = useState("");
|
|
221
223
|
const [codespaceName, setCodespaceName] = useState("");
|
|
224
|
+
const [githubAccountId, setGithubAccountId] = useState("");
|
|
222
225
|
|
|
223
226
|
// ─── Edit mode state ───────────────────────────────
|
|
224
227
|
|
|
@@ -286,7 +289,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
|
|
|
286
289
|
if (!isCreateValid()) {
|
|
287
290
|
return;
|
|
288
291
|
}
|
|
289
|
-
onAddEnvironment(envName.trim(), adapterType, buildCreateConfig());
|
|
292
|
+
onAddEnvironment(envName.trim(), adapterType, buildCreateConfig(), githubAccountId || undefined);
|
|
290
293
|
onShowToast?.("Environment added successfully", "success");
|
|
291
294
|
navigate(ENVIRONMENTS_URL, { replace: true });
|
|
292
295
|
};
|
|
@@ -406,6 +409,32 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
|
|
|
406
409
|
</span>
|
|
407
410
|
</div>
|
|
408
411
|
|
|
412
|
+
{/* GitHub Account (codespace and docker only). Show when accounts are
|
|
413
|
+
registered OR when the env already has an account association so
|
|
414
|
+
the user can clear it even if the referenced account was removed. */}
|
|
415
|
+
{(existingEnv.adapterType === "codespace" || existingEnv.adapterType === "docker") && (githubAccounts.length > 0 || Boolean(existingEnv.githubAccountId)) && (
|
|
416
|
+
<div className={styles.section}>
|
|
417
|
+
<label className={styles.label}>GitHub Account</label>
|
|
418
|
+
<select
|
|
419
|
+
value={existingEnv.githubAccountId || ""}
|
|
420
|
+
onChange={(e) => {
|
|
421
|
+
if (environmentId) {
|
|
422
|
+
onUpdateEnvironment(environmentId, { githubAccountId: e.target.value });
|
|
423
|
+
}
|
|
424
|
+
}}
|
|
425
|
+
className={styles.adapterSelect}
|
|
426
|
+
data-testid="env-edit-github-account"
|
|
427
|
+
>
|
|
428
|
+
<option value="">(Default)</option>
|
|
429
|
+
{githubAccounts.map((a) => (
|
|
430
|
+
<option key={a.id} value={a.id}>
|
|
431
|
+
{a.label}{a.username ? ` (@${a.username})` : ""}{a.isDefault ? " — default" : ""}
|
|
432
|
+
</option>
|
|
433
|
+
))}
|
|
434
|
+
</select>
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
409
438
|
{/* Adapter-specific editable fields */}
|
|
410
439
|
{existingEnv.adapterType === "local" && (
|
|
411
440
|
<>
|
|
@@ -620,7 +649,7 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
|
|
|
620
649
|
onChange={(e) => {
|
|
621
650
|
setAdapterType(e.target.value);
|
|
622
651
|
if (e.target.value === "codespace") {
|
|
623
|
-
onListCodespaces();
|
|
652
|
+
onListCodespaces(githubAccountId || undefined);
|
|
624
653
|
}
|
|
625
654
|
}}
|
|
626
655
|
className={styles.adapterSelect}
|
|
@@ -633,6 +662,34 @@ export function EnvironmentEditPanel({ mode, environmentId, environments, onAddE
|
|
|
633
662
|
</select>
|
|
634
663
|
</div>
|
|
635
664
|
|
|
665
|
+
{/* GitHub Account (codespace and docker only) */}
|
|
666
|
+
{(adapterType === "codespace" || adapterType === "docker") && githubAccounts.length > 0 && (
|
|
667
|
+
<div className={styles.section}>
|
|
668
|
+
<label className={styles.label} htmlFor="env-create-github-account">
|
|
669
|
+
GitHub Account
|
|
670
|
+
</label>
|
|
671
|
+
<select
|
|
672
|
+
id="env-create-github-account"
|
|
673
|
+
value={githubAccountId}
|
|
674
|
+
onChange={(e) => {
|
|
675
|
+
setGithubAccountId(e.target.value);
|
|
676
|
+
if (adapterType === "codespace") {
|
|
677
|
+
onListCodespaces(e.target.value || undefined);
|
|
678
|
+
}
|
|
679
|
+
}}
|
|
680
|
+
className={styles.adapterSelect}
|
|
681
|
+
data-testid="env-create-github-account"
|
|
682
|
+
>
|
|
683
|
+
<option value="">(Default)</option>
|
|
684
|
+
{githubAccounts.map((a) => (
|
|
685
|
+
<option key={a.id} value={a.id}>
|
|
686
|
+
{a.label} (@{a.username}){a.isDefault ? " — default" : ""}
|
|
687
|
+
</option>
|
|
688
|
+
))}
|
|
689
|
+
</select>
|
|
690
|
+
</div>
|
|
691
|
+
)}
|
|
692
|
+
|
|
636
693
|
{/* Adapter-specific fields */}
|
|
637
694
|
{adapterType === "local" && (
|
|
638
695
|
<>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useState, type JSX, type FormEvent } from "react";
|
|
2
|
+
import { X, Star } from "lucide-react";
|
|
3
|
+
import type { ToastVariant } from "../../context/ToastContext.js";
|
|
4
|
+
import type { GitHubAccountData } from "../../hooks/types.js";
|
|
5
|
+
import { ICON_MD } from "../../utils/iconSize.js";
|
|
6
|
+
import { ConfirmDialog } from "../display/index.js";
|
|
7
|
+
import styles from "./SettingsPanel.module.scss";
|
|
8
|
+
|
|
9
|
+
/** Props for the GitHubAccountsPanel component. */
|
|
10
|
+
export interface GitHubAccountsPanelProps {
|
|
11
|
+
/** All registered GitHub accounts. */
|
|
12
|
+
githubAccounts: GitHubAccountData[];
|
|
13
|
+
/** Whether the account list is loading. */
|
|
14
|
+
githubAccountsLoading: boolean;
|
|
15
|
+
/** Register a new GitHub account. Username will be resolved server-side if empty. */
|
|
16
|
+
onAddGitHubAccount: (label: string, token: string, username: string, isDefault: boolean) => Promise<void>;
|
|
17
|
+
/** Update an existing GitHub account. */
|
|
18
|
+
onUpdateGitHubAccount: (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => Promise<void>;
|
|
19
|
+
/** Remove a GitHub account by ID. */
|
|
20
|
+
onRemoveGitHubAccount: (id: string) => Promise<void>;
|
|
21
|
+
/** Import accounts from the local gh CLI authentication state. */
|
|
22
|
+
onImportGitHubAccounts: () => Promise<{ imported: number; usernames: string[] }>;
|
|
23
|
+
/** Display a toast notification. */
|
|
24
|
+
onShowToast?: (message: string, variant?: ToastVariant) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Settings panel for managing registered GitHub accounts. */
|
|
28
|
+
export function GitHubAccountsPanel({
|
|
29
|
+
githubAccounts,
|
|
30
|
+
githubAccountsLoading,
|
|
31
|
+
onAddGitHubAccount,
|
|
32
|
+
onUpdateGitHubAccount,
|
|
33
|
+
onRemoveGitHubAccount,
|
|
34
|
+
onImportGitHubAccounts,
|
|
35
|
+
onShowToast,
|
|
36
|
+
}: GitHubAccountsPanelProps): JSX.Element {
|
|
37
|
+
|
|
38
|
+
const [label, setLabel] = useState("");
|
|
39
|
+
const [token, setToken] = useState("");
|
|
40
|
+
const [isDefault, setIsDefault] = useState(false);
|
|
41
|
+
const [adding, setAdding] = useState(false);
|
|
42
|
+
const [importing, setImporting] = useState(false);
|
|
43
|
+
const [confirmRemoveId, setConfirmRemoveId] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
const confirmRemoveAccount = githubAccounts.find((a) => a.id === confirmRemoveId);
|
|
46
|
+
|
|
47
|
+
const handleSubmit = async (e: FormEvent): Promise<void> => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
if (!label.trim() || !token.trim()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setAdding(true);
|
|
53
|
+
try {
|
|
54
|
+
await onAddGitHubAccount(label.trim(), token.trim(), "", isDefault);
|
|
55
|
+
onShowToast?.("GitHub account added", "success");
|
|
56
|
+
setLabel("");
|
|
57
|
+
setToken("");
|
|
58
|
+
setIsDefault(false);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : "Failed to add account";
|
|
61
|
+
onShowToast?.(msg, "error");
|
|
62
|
+
} finally {
|
|
63
|
+
setAdding(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleSetDefault = async (id: string): Promise<void> => {
|
|
68
|
+
try {
|
|
69
|
+
await onUpdateGitHubAccount(id, { isDefault: true });
|
|
70
|
+
onShowToast?.("Default account updated", "success");
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : "Failed to update account";
|
|
73
|
+
onShowToast?.(msg, "error");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleConfirmRemove = async (): Promise<void> => {
|
|
78
|
+
if (!confirmRemoveId) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await onRemoveGitHubAccount(confirmRemoveId);
|
|
83
|
+
onShowToast?.("GitHub account removed", "info");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : "Failed to remove account";
|
|
86
|
+
onShowToast?.(msg, "error");
|
|
87
|
+
} finally {
|
|
88
|
+
setConfirmRemoveId(null);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleImport = async (): Promise<void> => {
|
|
93
|
+
setImporting(true);
|
|
94
|
+
try {
|
|
95
|
+
const result = await onImportGitHubAccounts();
|
|
96
|
+
if (result.imported > 0) {
|
|
97
|
+
onShowToast?.(`Imported ${result.imported} account(s): ${result.usernames.join(", ")}`, "success");
|
|
98
|
+
} else {
|
|
99
|
+
onShowToast?.("No new accounts to import", "info");
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const msg = err instanceof Error ? err.message : "Import failed";
|
|
103
|
+
onShowToast?.(msg, "error");
|
|
104
|
+
} finally {
|
|
105
|
+
setImporting(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<ConfirmDialog
|
|
112
|
+
isOpen={confirmRemoveId !== null}
|
|
113
|
+
title="Remove GitHub Account?"
|
|
114
|
+
description={
|
|
115
|
+
confirmRemoveAccount
|
|
116
|
+
? `"${confirmRemoveAccount.label}"${confirmRemoveAccount.username ? ` (@${confirmRemoveAccount.username})` : ""} will be permanently removed.`
|
|
117
|
+
: undefined
|
|
118
|
+
}
|
|
119
|
+
onConfirm={() => { handleConfirmRemove().catch(() => {}); }}
|
|
120
|
+
onCancel={() => setConfirmRemoveId(null)}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<section className={styles.section} data-testid="github-accounts-panel">
|
|
124
|
+
<h3 className={styles.sectionTitle}>GitHub Accounts</h3>
|
|
125
|
+
<p className={styles.sectionDescription}>
|
|
126
|
+
Register multiple GitHub accounts to use different identities per environment.
|
|
127
|
+
The default account is used when no specific account is assigned.
|
|
128
|
+
</p>
|
|
129
|
+
|
|
130
|
+
{githubAccountsLoading && githubAccounts.length === 0 ? (
|
|
131
|
+
<div className={styles.emptyState}>Loading...</div>
|
|
132
|
+
) : githubAccounts.length === 0 ? (
|
|
133
|
+
<div className={styles.emptyStateInfo}>
|
|
134
|
+
No GitHub accounts registered. Add one below or import from the gh CLI.
|
|
135
|
+
</div>
|
|
136
|
+
) : (
|
|
137
|
+
<div className={styles.tokenList}>
|
|
138
|
+
{githubAccounts.map((account) => (
|
|
139
|
+
<div key={account.id} className={styles.tokenRow} data-testid={`github-account-row-${account.id}`}>
|
|
140
|
+
{account.isDefault && (
|
|
141
|
+
<span className={styles.tokenBadge} title="Default account">default</span>
|
|
142
|
+
)}
|
|
143
|
+
<span className={styles.tokenName}>{account.label}</span>
|
|
144
|
+
{account.username && (
|
|
145
|
+
<span className={styles.tokenTarget}>@{account.username}</span>
|
|
146
|
+
)}
|
|
147
|
+
{!account.isDefault && (
|
|
148
|
+
<button
|
|
149
|
+
className={styles.deleteButton}
|
|
150
|
+
onClick={() => { handleSetDefault(account.id).catch(() => {}); }}
|
|
151
|
+
title="Set as default"
|
|
152
|
+
aria-label={`Set ${account.label} as default`}
|
|
153
|
+
data-testid={`github-account-set-default-${account.id}`}
|
|
154
|
+
>
|
|
155
|
+
<Star size={ICON_MD} aria-hidden="true" />
|
|
156
|
+
</button>
|
|
157
|
+
)}
|
|
158
|
+
<button
|
|
159
|
+
className={styles.deleteButton}
|
|
160
|
+
onClick={() => setConfirmRemoveId(account.id)}
|
|
161
|
+
title={`Remove ${account.label}`}
|
|
162
|
+
aria-label={`Remove ${account.label}`}
|
|
163
|
+
data-testid={`github-account-remove-${account.id}`}
|
|
164
|
+
>
|
|
165
|
+
<X size={ICON_MD} aria-hidden="true" />
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
<form className={styles.addForm} onSubmit={(e) => { handleSubmit(e).catch(() => {}); }}>
|
|
173
|
+
<div className={styles.formRow}>
|
|
174
|
+
<input
|
|
175
|
+
className={styles.input}
|
|
176
|
+
type="text"
|
|
177
|
+
placeholder="Label (e.g. personal, work)"
|
|
178
|
+
value={label}
|
|
179
|
+
onChange={(e) => setLabel(e.target.value)}
|
|
180
|
+
data-testid="github-account-label-input"
|
|
181
|
+
/>
|
|
182
|
+
<input
|
|
183
|
+
className={styles.input}
|
|
184
|
+
type="password"
|
|
185
|
+
placeholder="Personal access token"
|
|
186
|
+
value={token}
|
|
187
|
+
onChange={(e) => setToken(e.target.value)}
|
|
188
|
+
data-testid="github-account-token-input"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
<div className={styles.formRow}>
|
|
192
|
+
<label className={styles.tokenTarget} style={{ display: "flex", alignItems: "center", gap: "6px", cursor: "pointer" }}>
|
|
193
|
+
<input
|
|
194
|
+
type="checkbox"
|
|
195
|
+
checked={isDefault}
|
|
196
|
+
onChange={(e) => setIsDefault(e.target.checked)}
|
|
197
|
+
data-testid="github-account-default-checkbox"
|
|
198
|
+
/>
|
|
199
|
+
Set as default
|
|
200
|
+
</label>
|
|
201
|
+
<button
|
|
202
|
+
className={styles.addButton}
|
|
203
|
+
type="submit"
|
|
204
|
+
disabled={!label.trim() || !token.trim() || adding}
|
|
205
|
+
data-testid="github-account-add-button"
|
|
206
|
+
>
|
|
207
|
+
{adding ? "Adding..." : "Add Account"}
|
|
208
|
+
</button>
|
|
209
|
+
<button
|
|
210
|
+
className={styles.addButton}
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={() => { handleImport().catch(() => {}); }}
|
|
213
|
+
disabled={importing}
|
|
214
|
+
data-testid="github-account-import-button"
|
|
215
|
+
>
|
|
216
|
+
{importing ? "Importing..." : "Import from gh CLI"}
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
</form>
|
|
220
|
+
</section>
|
|
221
|
+
</>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -11,3 +11,5 @@ export { TaskActionButtons } from "./TaskActionButtons.js";
|
|
|
11
11
|
export { TaskOverviewPanel } from "./TaskOverviewPanel.js";
|
|
12
12
|
export { PluginsPanel } from "./PluginsPanel.js";
|
|
13
13
|
export type { PluginsPanelProps } from "./PluginsPanel.js";
|
|
14
|
+
export { GitHubAccountsPanel } from "./GitHubAccountsPanel.js";
|
|
15
|
+
export type { GitHubAccountsPanelProps } from "./GitHubAccountsPanel.js";
|
|
@@ -9,10 +9,11 @@ const meta: Meta<typeof SettingsNav> = {
|
|
|
9
9
|
export default meta;
|
|
10
10
|
type Story = StoryObj<typeof meta>;
|
|
11
11
|
|
|
12
|
-
/** All
|
|
12
|
+
/** All tabs are rendered with correct labels. */
|
|
13
13
|
export const AllTabsRendered: Story = {
|
|
14
14
|
play: async ({ canvas }) => {
|
|
15
15
|
await expect(canvas.getByRole("tab", { name: /Credentials/ })).toBeInTheDocument();
|
|
16
|
+
await expect(canvas.getByRole("tab", { name: /GitHub Accounts/ })).toBeInTheDocument();
|
|
16
17
|
await expect(canvas.getByRole("tab", { name: /Personas/ })).toBeInTheDocument();
|
|
17
18
|
await expect(canvas.getByRole("tab", { name: /Schedules/ })).toBeInTheDocument();
|
|
18
19
|
await expect(canvas.getByRole("tab", { name: /Appearance/ })).toBeInTheDocument();
|
|
@@ -35,15 +36,15 @@ export const KeyboardNavigation: Story = {
|
|
|
35
36
|
const credentialsTab = canvas.getByRole("tab", { name: /Credentials/ });
|
|
36
37
|
credentialsTab.focus();
|
|
37
38
|
|
|
38
|
-
// ArrowDown should move to
|
|
39
|
+
// ArrowDown should move to GitHub (now second tab)
|
|
39
40
|
await userEvent.keyboard("{ArrowDown}");
|
|
40
|
-
const
|
|
41
|
-
await expect(
|
|
41
|
+
const githubTab = canvas.getByRole("tab", { name: /GitHub Accounts/ });
|
|
42
|
+
await expect(githubTab).toHaveFocus();
|
|
42
43
|
|
|
43
|
-
// ArrowDown again to
|
|
44
|
+
// ArrowDown again to Personas
|
|
44
45
|
await userEvent.keyboard("{ArrowDown}");
|
|
45
|
-
const
|
|
46
|
-
await expect(
|
|
46
|
+
const personasTab = canvas.getByRole("tab", { name: /Personas/ });
|
|
47
|
+
await expect(personasTab).toHaveFocus();
|
|
47
48
|
|
|
48
49
|
// Home goes to first tab
|
|
49
50
|
await userEvent.keyboard("{Home}");
|
|
@@ -62,10 +63,10 @@ export const JKNavigation: Story = {
|
|
|
62
63
|
const credentialsTab = canvas.getByRole("tab", { name: /Credentials/ });
|
|
63
64
|
credentialsTab.focus();
|
|
64
65
|
|
|
65
|
-
// J moves down to
|
|
66
|
+
// J moves down to GitHub (now second tab)
|
|
66
67
|
await userEvent.keyboard("j");
|
|
67
|
-
const
|
|
68
|
-
await expect(
|
|
68
|
+
const githubTab = canvas.getByRole("tab", { name: /GitHub Accounts/ });
|
|
69
|
+
await expect(githubTab).toHaveFocus();
|
|
69
70
|
|
|
70
71
|
// K moves back up to Credentials
|
|
71
72
|
await userEvent.keyboard("k");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef, type JSX, type KeyboardEvent, type ReactNode } from "react";
|
|
2
2
|
import { useLocation } from "react-router";
|
|
3
|
-
import { CalendarClock, Info, Key, Keyboard, Palette, Puzzle, User } from "lucide-react";
|
|
3
|
+
import { CalendarClock, Github, Info, Key, Keyboard, Palette, Puzzle, User } from "lucide-react";
|
|
4
4
|
import { SETTINGS_URL, useAppNavigate } from "../../utils/navigation.js";
|
|
5
5
|
import { ICON_LG } from "../../utils/iconSize.js";
|
|
6
6
|
import styles from "./SettingsNav.module.scss";
|
|
@@ -18,6 +18,7 @@ interface SettingsTab {
|
|
|
18
18
|
/** Ordered list of settings tabs. */
|
|
19
19
|
const TABS: SettingsTab[] = [
|
|
20
20
|
{ path: "credentials", label: "Credentials", icon: <Key size={ICON_LG} /> },
|
|
21
|
+
{ path: "github-accounts", label: "GitHub Accounts", icon: <Github size={ICON_LG} /> },
|
|
21
22
|
{ path: "personas", label: "Personas", icon: <User size={ICON_LG} /> },
|
|
22
23
|
{ path: "schedules", label: "Schedules", icon: <CalendarClock size={ICON_LG} /> },
|
|
23
24
|
{ path: "appearance", label: "Appearance", icon: <Palette size={ICON_LG} /> },
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
UsePluginsResult,
|
|
15
15
|
UseSchedulesResult,
|
|
16
16
|
UseStreamsResult,
|
|
17
|
+
UseGitHubAccountsResult,
|
|
17
18
|
ConnectionStatus,
|
|
18
19
|
} from "../hooks/types.js";
|
|
19
20
|
|
|
@@ -47,6 +48,8 @@ export interface UseGrackleSocketResult {
|
|
|
47
48
|
knowledge: Omit<UseKnowledgeResult, "handleEvent">;
|
|
48
49
|
/** Plugin state and actions. */
|
|
49
50
|
plugins: Omit<UsePluginsResult, "domainHook">;
|
|
51
|
+
/** GitHub account state and actions. */
|
|
52
|
+
githubAccounts: Omit<UseGitHubAccountsResult, "domainHook">;
|
|
50
53
|
/** App-level default persona ID setting. */
|
|
51
54
|
appDefaultPersonaId: string;
|
|
52
55
|
/** Update the app-level default persona ID. */
|