Skip to content

Commit 904e670

Browse files
committed
fix(gui): address Codex review on dense workspaces stack
1 parent a2072d5 commit 904e670

46 files changed

Lines changed: 5294 additions & 969 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* ApiKeysWorkspace — rail + main for the API tab. Overview hosts the existing
3+
* endpoint/auth/generate/models/usage panels; selecting a key opens detail.
4+
*/
5+
import { useState } from "react";
6+
import { IconChevron, IconTrash } from "../../icons";
7+
import { useT } from "../../i18n/shared";
8+
import type { ExternalModelRow } from "../../api-access-models";
9+
import {
10+
formatCreatedDate,
11+
type ApiEndpointInfo,
12+
type ApiKeyEntry,
13+
type ModelTestState,
14+
} from "../../pages/api-keys-utils";
15+
import {
16+
ApiKeysEndpointsPanel,
17+
ApiKeysManagePanel,
18+
ApiKeysModelsPanel,
19+
ApiKeysUsagePanel,
20+
} from "../../pages/api-keys-panels";
21+
22+
export interface ApiKeysWorkspaceProps {
23+
keys: ApiKeyEntry[];
24+
keysLoading: boolean;
25+
keysLoadFailed: boolean;
26+
endpoints: ApiEndpointInfo;
27+
claudeCodeEnabled: boolean;
28+
localeTag?: string;
29+
newName: string;
30+
creating: boolean;
31+
newKey: string | null;
32+
copied: boolean;
33+
filteredModels: ExternalModelRow[];
34+
modelsLoading: boolean;
35+
modelsLoadFailed: boolean;
36+
modelQuery: string;
37+
copiedModelId: string | null;
38+
modelTests: Record<string, { state: ModelTestState; detail?: string }>;
39+
onNewNameChange: (value: string) => void;
40+
onCreate: () => void;
41+
onDismissNewKey: () => void;
42+
onCopyKey: () => void;
43+
onDelete: (id: string) => void;
44+
onModelQueryChange: (value: string) => void;
45+
onCopyModelId: (modelId: string) => void;
46+
onTestModel: (model: ExternalModelRow) => void;
47+
sourceLabel: (model: ExternalModelRow) => string;
48+
protocolLabel: (protocol: string) => string;
49+
}
50+
51+
export default function ApiKeysWorkspace({
52+
keys,
53+
keysLoading,
54+
keysLoadFailed,
55+
endpoints,
56+
claudeCodeEnabled,
57+
localeTag,
58+
newName,
59+
creating,
60+
newKey,
61+
copied,
62+
filteredModels,
63+
modelsLoading,
64+
modelsLoadFailed,
65+
modelQuery,
66+
copiedModelId,
67+
modelTests,
68+
onNewNameChange,
69+
onCreate,
70+
onDismissNewKey,
71+
onCopyKey,
72+
onDelete,
73+
onModelQueryChange,
74+
onCopyModelId,
75+
onTestModel,
76+
sourceLabel,
77+
protocolLabel,
78+
}: ApiKeysWorkspaceProps) {
79+
const t = useT();
80+
const [selectedId, setSelectedId] = useState<string | null>(null);
81+
const [confirmDelete, setConfirmDelete] = useState(false);
82+
83+
const selected = selectedId ? (keys.find(k => k.id === selectedId) ?? null) : null;
84+
85+
const showOverview = () => {
86+
setSelectedId(null);
87+
setConfirmDelete(false);
88+
};
89+
90+
const handleDeleteClick = () => {
91+
if (!selected) return;
92+
if (!confirmDelete) {
93+
setConfirmDelete(true);
94+
return;
95+
}
96+
onDelete(selected.id);
97+
setConfirmDelete(false);
98+
setSelectedId(null);
99+
};
100+
101+
return (
102+
<div className="apikeys-workspace-shell">
103+
<div className="apikeys-workspace-root">
104+
<aside className="apikeys-workspace-rail" aria-label={t("api.title")}>
105+
<div className="apikeys-workspace-rail-header">
106+
<span className="apikeys-workspace-rail-title">
107+
{keysLoading ? t("api.activeKeysLoading") : t("api.activeKeys", { count: keys.length })}
108+
</span>
109+
</div>
110+
<div className="apikeys-workspace-rail-list">
111+
<button
112+
type="button"
113+
className={`apikeys-workspace-rail-row${selectedId === null ? " apikeys-workspace-rail-row--selected" : ""}`}
114+
onClick={showOverview}
115+
aria-current={selectedId === null ? "page" : undefined}
116+
>
117+
<span className="apikeys-workspace-rail-name">{t("api.workspace.overview")}</span>
118+
</button>
119+
{keysLoading ? (
120+
<span className="apikeys-workspace-rail-empty">{t("common.loading")}</span>
121+
) : keys.length === 0 ? (
122+
<span className="apikeys-workspace-rail-empty">
123+
{keysLoadFailed ? t("api.keysLoadFailed") : t("api.workspace.noKeysHint")}
124+
</span>
125+
) : (
126+
keys.map(k => (
127+
<button
128+
key={k.id}
129+
type="button"
130+
className={`apikeys-workspace-rail-row${selectedId === k.id ? " apikeys-workspace-rail-row--selected" : ""}`}
131+
onClick={() => { setSelectedId(k.id); setConfirmDelete(false); }}
132+
aria-current={selectedId === k.id ? "page" : undefined}
133+
>
134+
<span className="apikeys-workspace-rail-name">{k.name}</span>
135+
<span className="apikeys-workspace-rail-meta">
136+
{k.prefix} · {formatCreatedDate(k.createdAt, localeTag)}
137+
</span>
138+
</button>
139+
))
140+
)}
141+
</div>
142+
</aside>
143+
144+
<section className="apikeys-workspace-main" aria-label={t("api.workspace.details")}>
145+
{selected ? (
146+
<div className="awi-detail">
147+
<div className="awi-detail-toolbar">
148+
<button type="button" className="awi-back" onClick={showOverview}>
149+
<IconChevron className="awi-back-chevron" aria-hidden="true" />
150+
{t("modal.back")}
151+
</button>
152+
</div>
153+
<div className="awi-detail-body">
154+
<div className="awi-detail-head">
155+
<h2 className="awi-detail-title">{selected.name}</h2>
156+
<span className="awi-detail-actions">
157+
{confirmDelete ? (
158+
<>
159+
<button type="button" className="btn btn-danger btn-sm" onClick={handleDeleteClick}>
160+
<IconTrash /> {t("api.confirm")}
161+
</button>
162+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => setConfirmDelete(false)}>
163+
{t("common.cancel")}
164+
</button>
165+
</>
166+
) : (
167+
<button
168+
type="button"
169+
className="btn btn-danger btn-sm"
170+
onClick={handleDeleteClick}
171+
aria-label={t("api.deleteAria")}
172+
>
173+
<IconTrash /> {t("api.workspace.deleteKey")}
174+
</button>
175+
)}
176+
</span>
177+
</div>
178+
{confirmDelete && (
179+
<p className="muted awi-delete-hint">{t("api.workspace.deleteConfirm")}</p>
180+
)}
181+
<div className="awi-section">
182+
<h3 className="awi-section-title">{t("api.workspace.keyDetails")}</h3>
183+
<dl className="awi-kv">
184+
<div className="awi-kv-row">
185+
<dt>{t("api.colName")}</dt>
186+
<dd>{selected.name}</dd>
187+
</div>
188+
<div className="awi-kv-row">
189+
<dt>{t("api.workspace.keyPrefix")}</dt>
190+
<dd><code>{selected.prefix}</code></dd>
191+
</div>
192+
<div className="awi-kv-row">
193+
<dt>{t("api.colCreated")}</dt>
194+
<dd>{formatCreatedDate(selected.createdAt, localeTag)}</dd>
195+
</div>
196+
</dl>
197+
</div>
198+
</div>
199+
</div>
200+
) : (
201+
<div className="awi-overview">
202+
<div className="awi-overview-left">
203+
<ApiKeysManagePanel
204+
keys={keys}
205+
keysLoading={keysLoading}
206+
keysLoadFailed={keysLoadFailed}
207+
newName={newName}
208+
creating={creating}
209+
newKey={newKey}
210+
copied={copied}
211+
confirmDelete={null}
212+
localeTag={localeTag}
213+
showKeyList={false}
214+
onNewNameChange={onNewNameChange}
215+
onCreate={onCreate}
216+
onDismissNewKey={onDismissNewKey}
217+
onCopyKey={onCopyKey}
218+
onConfirmDelete={() => {}}
219+
onCancelDelete={() => {}}
220+
onDelete={() => {}}
221+
/>
222+
<ApiKeysEndpointsPanel endpoints={endpoints} claudeCodeEnabled={claudeCodeEnabled} />
223+
<ApiKeysUsagePanel endpoints={endpoints} claudeCodeEnabled={claudeCodeEnabled} />
224+
</div>
225+
<div className="awi-overview-right">
226+
<ApiKeysModelsPanel
227+
filteredModels={filteredModels}
228+
modelsLoading={modelsLoading}
229+
modelsLoadFailed={modelsLoadFailed}
230+
modelQuery={modelQuery}
231+
copiedModelId={copiedModelId}
232+
modelTests={modelTests}
233+
claudeCodeEnabled={claudeCodeEnabled}
234+
onModelQueryChange={onModelQueryChange}
235+
onCopyModelId={onCopyModelId}
236+
onTestModel={onTestModel}
237+
sourceLabel={sourceLabel}
238+
protocolLabel={protocolLabel}
239+
/>
240+
</div>
241+
</div>
242+
)}
243+
</section>
244+
</div>
245+
</div>
246+
);
247+
}

0 commit comments

Comments
 (0)