Skip to content

Commit 6f4f433

Browse files
committed
feature(env-vars-editor): add inherited values support to environment variables editor
1 parent 7e08c34 commit 6f4f433

2 files changed

Lines changed: 70 additions & 15 deletions

File tree

control-plane/frontend/src/components/EnvVarsEditor.tsx

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useMemo, useRef, useState } from "react";
2-
import { Trash2 } from "lucide-react";
2+
import { ChevronDown, ChevronRight, Eye, EyeOff, Trash2 } from "lucide-react";
33

44
// Keep in sync with ReservedEnvVarNames in control-plane/internal/handlers/envvars.go.
55
const RESERVED = new Set([
@@ -19,6 +19,8 @@ export interface EnvVarsDelta {
1919
interface Props {
2020
/** Current plaintext values from the API. */
2121
values: Record<string, string>;
22+
/** Read-only values inherited from a broader scope, e.g. global settings. */
23+
inheritedValues?: Record<string, string>;
2224
/** Section title shown in the card header. */
2325
title: string;
2426
/** Help text rendered below the title. */
@@ -70,6 +72,7 @@ function buildInitialRows(values: Record<string, string>): EditRow[] {
7072

7173
export default function EnvVarsEditor({
7274
values,
75+
inheritedValues = {},
7376
title,
7477
description,
7578
onSave,
@@ -82,6 +85,8 @@ export default function EnvVarsEditor({
8285
const [rows, setRows] = useState<EditRow[]>(() =>
8386
inline ? buildInitialRows(values) : [],
8487
);
88+
const [showValues, setShowValues] = useState(false);
89+
const [showInheritedValues, setShowInheritedValues] = useState(false);
8590
const [error, setError] = useState<string | null>(null);
8691

8792
// In inline mode, report the current valid map upward whenever rows change.
@@ -227,34 +232,81 @@ export default function EnvVarsEditor({
227232
};
228233

229234
const valueKeys = useMemo(() => Object.keys(values).sort(), [values]);
235+
const inheritedKeys = useMemo(
236+
() => Object.keys(inheritedValues).filter((key) => values[key] === undefined).sort(),
237+
[inheritedValues, values],
238+
);
230239

231240
return (
232241
<div className="bg-white rounded-lg border border-gray-200 p-6">
233242
<div className="flex items-center justify-between mb-2">
234243
<h3 className="text-sm font-medium text-gray-900">{title}</h3>
235244
{!inline && mode === "display" && (
236-
<button
237-
type="button"
238-
onClick={beginEdit}
239-
className="text-xs text-blue-600 hover:text-blue-800"
240-
>
241-
Edit
242-
</button>
245+
<div className="flex items-center gap-3">
246+
<button
247+
type="button"
248+
onClick={() => setShowValues((prev) => !prev)}
249+
className="text-gray-400 hover:text-gray-600"
250+
title={showValues ? "Hide values" : "Show values"}
251+
aria-label={showValues ? "Hide values" : "Show values"}
252+
>
253+
{showValues ? <EyeOff size={14} /> : <Eye size={14} />}
254+
</button>
255+
<button
256+
type="button"
257+
onClick={beginEdit}
258+
className="text-xs text-blue-600 hover:text-blue-800"
259+
>
260+
Edit
261+
</button>
262+
</div>
243263
)}
244264
</div>
245265
<p className="text-xs text-gray-500 mb-4">{description}</p>
246266

247267
{mode === "display" ? (
248-
valueKeys.length === 0 ? (
268+
valueKeys.length === 0 && inheritedKeys.length === 0 ? (
249269
<p className="text-sm text-gray-400 italic">{emptyMessage}</p>
250270
) : (
251-
<div className="divide-y divide-gray-100">
252-
{valueKeys.map((k) => (
253-
<div key={k} className="py-2 flex items-center justify-between gap-4">
254-
<span className="text-sm font-mono text-gray-900">{k}</span>
255-
<span className="text-xs font-mono text-gray-500 truncate">{values[k]}</span>
271+
<div className="space-y-4">
272+
{valueKeys.length > 0 && (
273+
<div>
274+
<p className="text-xs font-medium uppercase tracking-wide text-gray-500 mb-2">
275+
Instance
276+
</p>
277+
<div className="divide-y divide-gray-100">
278+
{valueKeys.map((k) => (
279+
<div key={k} className="py-2 flex items-center justify-between gap-4">
280+
<span className="text-sm font-mono text-gray-900">{k}</span>
281+
<span className="text-xs font-mono text-gray-500 truncate">{showValues ? values[k] : "••••••••"}</span>
282+
</div>
283+
))}
284+
</div>
285+
</div>
286+
)}
287+
288+
{inheritedKeys.length > 0 && (
289+
<div>
290+
<button
291+
type="button"
292+
onClick={() => setShowInheritedValues((prev) => !prev)}
293+
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-gray-500 mb-2 hover:text-gray-700"
294+
>
295+
{showInheritedValues ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
296+
<span>Inherited From Global Settings</span>
297+
</button>
298+
{showInheritedValues && (
299+
<div className="divide-y divide-gray-100">
300+
{inheritedKeys.map((k) => (
301+
<div key={k} className="py-2 flex items-center justify-between gap-4">
302+
<span className="text-sm font-mono text-gray-700">{k}</span>
303+
<span className="text-xs font-mono text-gray-400 truncate">{showValues ? inheritedValues[k] : "••••••••"}</span>
304+
</div>
305+
))}
306+
</div>
307+
)}
256308
</div>
257-
))}
309+
)}
258310
</div>
259311
)
260312
) : (

control-plane/frontend/src/pages/InstanceDetailPage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
useUpdateInstanceImage,
3939
} from "@/hooks/useInstances";
4040
import { useProviders } from "@/hooks/useProviders";
41+
import { useSettings } from "@/hooks/useSettings";
4142
import { useQueries, useQueryClient } from "@tanstack/react-query";
4243
import { fetchCatalogProviderDetail } from "@/api/llm";
4344
import type { CatalogProviderDetail } from "@/api/llm";
@@ -69,6 +70,7 @@ export default function InstanceDetailPage() {
6970
const qc = useQueryClient();
7071
const { isAdmin } = useAuth();
7172
const { data: instance, isLoading } = useInstance(instanceId);
73+
const { data: settings } = useSettings();
7274
const { data: allProviders = [] } = useProviders();
7375

7476
// Fetch catalog model lists for all catalog providers (used in edit mode)
@@ -829,6 +831,7 @@ export default function InstanceDetailPage() {
829831
{/* Environment Variables */}
830832
<EnvVarsEditor
831833
values={instance.env_vars ?? {}}
834+
inheritedValues={settings?.default_env_vars ?? {}}
832835
title="Environment Variables"
833836
description="Per-instance values override globals with the same name. Values are encrypted at rest. Saving restarts this instance so the change takes effect immediately."
834837
onSave={handleSaveEnvVars}

0 commit comments

Comments
 (0)