@grackle-ai/web-components 0.107.2 → 0.108.0
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/{3ae72563f781afd72723475938136f113846603e.untar.log → 6a006f2c0da21b52033b551b9ab7fe5476f98104.untar.log} +2 -2
- package/.rush/temp/{bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log → 8d311efca0ab9b63564c69382ef5e7be719f9d07.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 +15 -15
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +15 -15
- 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_test.cache.log +1 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grackle-ai/web-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.108.0",
|
|
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.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@rushstack/heft": "1.2.7",
|
|
@@ -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. */
|
package/src/hooks/types.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface Environment {
|
|
|
32
32
|
adapterConfig: string;
|
|
33
33
|
status: string;
|
|
34
34
|
bootstrapped: boolean;
|
|
35
|
+
/** ID of the GitHub account to use for gh CLI operations, or empty string for default. */
|
|
36
|
+
githubAccountId: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
|
|
@@ -223,11 +225,12 @@ export interface UseEnvironmentsResult {
|
|
|
223
225
|
displayName: string,
|
|
224
226
|
adapterType: string,
|
|
225
227
|
adapterConfig?: Record<string, unknown>,
|
|
228
|
+
githubAccountId?: string,
|
|
226
229
|
) => Promise<void>;
|
|
227
230
|
/** Update an existing environment's mutable fields. */
|
|
228
231
|
updateEnvironment: (
|
|
229
232
|
environmentId: string,
|
|
230
|
-
fields: { displayName?: string; adapterConfig?: Record<string, unknown
|
|
233
|
+
fields: { displayName?: string; adapterConfig?: Record<string, unknown>; githubAccountId?: string },
|
|
231
234
|
) => Promise<void>;
|
|
232
235
|
/** Provision an environment by ID. When force is true, kills active sessions and forces full provision. */
|
|
233
236
|
provisionEnvironment: (environmentId: string, force?: boolean) => Promise<void>;
|
|
@@ -269,6 +272,7 @@ export interface UseSessionsResult {
|
|
|
269
272
|
prompt: string,
|
|
270
273
|
personaId?: string,
|
|
271
274
|
workingDirectory?: string,
|
|
275
|
+
workspaceId?: string,
|
|
272
276
|
) => Promise<void>;
|
|
273
277
|
/** Send text input to a running session. */
|
|
274
278
|
sendInput: (sessionId: string, text: string) => Promise<void>;
|
|
@@ -484,8 +488,8 @@ export interface UseCodespacesResult {
|
|
|
484
488
|
codespaceListError: string;
|
|
485
489
|
/** Whether a codespace creation is currently in progress. */
|
|
486
490
|
codespaceCreating: boolean;
|
|
487
|
-
/** Request the current codespace list from the server. */
|
|
488
|
-
listCodespaces: () => Promise<void>;
|
|
491
|
+
/** Request the current codespace list from the server, optionally filtered to a GitHub account. */
|
|
492
|
+
listCodespaces: (githubAccountId?: string) => Promise<void>;
|
|
489
493
|
/** Create a new codespace for the given repo. */
|
|
490
494
|
createCodespace: (repo: string, machine?: string) => Promise<void>;
|
|
491
495
|
/** Lifecycle hook for connect/disconnect/event routing. */
|
|
@@ -854,6 +858,35 @@ export interface UsePluginsResult {
|
|
|
854
858
|
domainHook: DomainHook;
|
|
855
859
|
}
|
|
856
860
|
|
|
861
|
+
/** A registered GitHub account (token is never returned by the server). */
|
|
862
|
+
export interface GitHubAccountData {
|
|
863
|
+
id: string;
|
|
864
|
+
label: string;
|
|
865
|
+
username: string;
|
|
866
|
+
isDefault: boolean;
|
|
867
|
+
createdAt: string;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Values returned by the GitHub accounts domain hook. */
|
|
871
|
+
export interface UseGitHubAccountsResult {
|
|
872
|
+
/** All registered GitHub accounts. */
|
|
873
|
+
githubAccounts: GitHubAccountData[];
|
|
874
|
+
/** Whether the account list is currently loading. */
|
|
875
|
+
githubAccountsLoading: boolean;
|
|
876
|
+
/** Refresh the account list from the server. */
|
|
877
|
+
loadGitHubAccounts: () => Promise<void>;
|
|
878
|
+
/** Register a new GitHub account. */
|
|
879
|
+
addGitHubAccount: (label: string, token: string, username: string, isDefault: boolean) => Promise<void>;
|
|
880
|
+
/** Update an existing GitHub account. */
|
|
881
|
+
updateGitHubAccount: (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => Promise<void>;
|
|
882
|
+
/** Remove a GitHub account by ID. */
|
|
883
|
+
removeGitHubAccount: (id: string) => Promise<void>;
|
|
884
|
+
/** Import accounts from the local gh CLI authentication state. */
|
|
885
|
+
importGitHubAccounts: () => Promise<{ imported: number; usernames: string[] }>;
|
|
886
|
+
/** Lifecycle hook for connect/disconnect/event routing. */
|
|
887
|
+
domainHook: DomainHook;
|
|
888
|
+
}
|
|
889
|
+
|
|
857
890
|
/** Delay in milliseconds before attempting a WebSocket reconnect. */
|
|
858
891
|
export const WS_RECONNECT_DELAY_MS: number = 3_000;
|
|
859
892
|
|
package/src/index.ts
CHANGED
|
@@ -63,8 +63,8 @@ export type { CalloutVariant } from "./components/notifications/index.js";
|
|
|
63
63
|
export { UpdateBanner } from "./components/notifications/UpdateBanner.js";
|
|
64
64
|
|
|
65
65
|
// Panels
|
|
66
|
-
export { FindingsPanel, TokensPanel, AppearancePanel, AboutPanel, TaskEditPanel, TaskActionButtons, TaskOverviewPanel, PluginsPanel } from "./components/panels/index.js";
|
|
67
|
-
export type { PluginsPanelProps } from "./components/panels/index.js";
|
|
66
|
+
export { FindingsPanel, TokensPanel, AppearancePanel, AboutPanel, TaskEditPanel, TaskActionButtons, TaskOverviewPanel, PluginsPanel, GitHubAccountsPanel } from "./components/panels/index.js";
|
|
67
|
+
export type { PluginsPanelProps, GitHubAccountsPanelProps } from "./components/panels/index.js";
|
|
68
68
|
export type { TaskActionButtonsProps } from "./components/panels/TaskActionButtons.js";
|
|
69
69
|
export type { TaskOverviewPanelProps } from "./components/panels/TaskOverviewPanel.js";
|
|
70
70
|
export { EnvironmentEditPanel } from "./components/panels/EnvironmentEditPanel.js";
|
|
@@ -134,6 +134,7 @@ export type {
|
|
|
134
134
|
UseCredentialsResult, UseCodespacesResult, UsePersonasResult,
|
|
135
135
|
UsePluginsResult, PluginData,
|
|
136
136
|
StreamData, StreamSubscriberData, UseStreamsResult,
|
|
137
|
+
UseGitHubAccountsResult, GitHubAccountData,
|
|
137
138
|
DomainHook,
|
|
138
139
|
ConnectionStatus,
|
|
139
140
|
} from "./hooks/types.js";
|
|
@@ -156,7 +157,7 @@ export {
|
|
|
156
157
|
useAppNavigate, sessionUrl, workspaceUrl, taskUrl, taskEditUrl,
|
|
157
158
|
newTaskUrl, newChatUrl, ENVIRONMENTS_URL, NEW_ENVIRONMENT_URL,
|
|
158
159
|
environmentUrl, environmentEditUrl, SETTINGS_URL,
|
|
159
|
-
SETTINGS_ENVIRONMENTS_URL, SETTINGS_CREDENTIALS_URL,
|
|
160
|
+
SETTINGS_ENVIRONMENTS_URL, SETTINGS_CREDENTIALS_URL, SETTINGS_GITHUB_ACCOUNTS_URL,
|
|
160
161
|
PERSONAS_URL, NEW_PERSONA_URL, personaUrl,
|
|
161
162
|
SCHEDULES_URL, NEW_SCHEDULE_URL, scheduleUrl,
|
|
162
163
|
SETTINGS_APPEARANCE_URL, SETTINGS_ABOUT_URL, SETTINGS_SHORTCUTS_URL,
|
|
@@ -1300,6 +1300,27 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
|
|
|
1300
1300
|
domainHook: NOOP_DOMAIN_HOOK,
|
|
1301
1301
|
},
|
|
1302
1302
|
|
|
1303
|
+
// ── GitHub Accounts ──────────────────────────────
|
|
1304
|
+
|
|
1305
|
+
githubAccounts: {
|
|
1306
|
+
githubAccounts: [],
|
|
1307
|
+
githubAccountsLoading: false,
|
|
1308
|
+
loadGitHubAccounts: async () => { console.log("[MockGrackle] loadGitHubAccounts"); },
|
|
1309
|
+
addGitHubAccount: async (label: string, _token: string, _username: string, _isDefault: boolean) => {
|
|
1310
|
+
console.log("[MockGrackle] addGitHubAccount", label);
|
|
1311
|
+
},
|
|
1312
|
+
updateGitHubAccount: async (id: string, fields: { label?: string; token?: string; isDefault?: boolean }) => {
|
|
1313
|
+
console.log("[MockGrackle] updateGitHubAccount", id, fields);
|
|
1314
|
+
},
|
|
1315
|
+
removeGitHubAccount: async (id: string) => {
|
|
1316
|
+
console.log("[MockGrackle] removeGitHubAccount", id);
|
|
1317
|
+
},
|
|
1318
|
+
importGitHubAccounts: async () => {
|
|
1319
|
+
console.log("[MockGrackle] importGitHubAccounts");
|
|
1320
|
+
return { imported: 0, usernames: [] };
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
|
|
1303
1324
|
// ── Plugins ─────────────────────────────────────
|
|
1304
1325
|
|
|
1305
1326
|
plugins: {
|
package/src/mocks/mockData.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
|
|
|
29
29
|
adapterConfig: "{}",
|
|
30
30
|
status: "connected",
|
|
31
31
|
bootstrapped: true,
|
|
32
|
+
githubAccountId: "",
|
|
32
33
|
},
|
|
33
34
|
{
|
|
34
35
|
id: "env-docker-01",
|
|
@@ -37,6 +38,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
|
|
|
37
38
|
adapterConfig: '{"image":"node:20"}',
|
|
38
39
|
status: "connected",
|
|
39
40
|
bootstrapped: true,
|
|
41
|
+
githubAccountId: "",
|
|
40
42
|
},
|
|
41
43
|
{
|
|
42
44
|
id: "env-cs-01",
|
|
@@ -45,6 +47,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
|
|
|
45
47
|
adapterConfig: '{"codespaceName":"my-codespace"}',
|
|
46
48
|
status: "connected",
|
|
47
49
|
bootstrapped: true,
|
|
50
|
+
githubAccountId: "",
|
|
48
51
|
},
|
|
49
52
|
{
|
|
50
53
|
id: "env-remote-01",
|
|
@@ -53,6 +56,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
|
|
|
53
56
|
adapterConfig: '{"host":"192.168.1.10","user":"deploy","sshPort":22}',
|
|
54
57
|
status: "disconnected",
|
|
55
58
|
bootstrapped: false,
|
|
59
|
+
githubAccountId: "",
|
|
56
60
|
},
|
|
57
61
|
{
|
|
58
62
|
id: "error-env",
|
|
@@ -61,6 +65,7 @@ export const MOCK_ENVIRONMENTS: Environment[] = [
|
|
|
61
65
|
adapterConfig: "{}",
|
|
62
66
|
status: "connected",
|
|
63
67
|
bootstrapped: true,
|
|
68
|
+
githubAccountId: "",
|
|
64
69
|
},
|
|
65
70
|
];
|
|
66
71
|
|