feat(service): variables editor with masked-secret preservation

PUT /services/{id}/variables replaced every row, and GET masks secrets as
******** — echoing the fetched list back would have overwritten real
secrets with asterisks. The backend now keeps the stored value when a
submitted secret still carries the mask.

Service detail gains a Variables section: add/edit/remove rows, secret
toggle, bulk .env paste (comments, export prefix, quoted values), and
inline validation for empty/invalid/duplicate keys. Parser and validator
live in variable-utils.ts with unit coverage.

Generated OpenAPI types drive the new updateServiceVariables client call.
ROADMAP: Variables UX marked done; per-environment values are already
structural — each environment owns its service instances.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 10:49:45 +02:00
1 parent e6e76c1084
commit 1d5b99e1ba
6 files changed
+354 -13

No files matched your search

+20 -7
View File
@@ -936,16 +936,12 @@ export async function deleteService(serviceId: string): Promise<void> {
});
}
export async function listServiceVariables(serviceId: string): Promise<ServiceVariable[]> {
const payload = await requestJson<{ variables?: RawServiceVariable[] }>(`/services/${serviceId}/variables`);
const rows = payload.variables ?? [];
function normalizeServiceVariables(rows: RawServiceVariable[] | undefined): ServiceVariable[] {
const result: ServiceVariable[] = [];
for (const row of rows) {
for (const row of rows ?? []) {
if (!row.id || !row.service_id || !row.key) {
continue;
}
result.push({
id: row.id,
serviceId: row.service_id,
@@ -956,10 +952,27 @@ export async function listServiceVariables(serviceId: string): Promise<ServiceVa
updatedAt: row.updated_at,
});
}
return result;
}
export async function listServiceVariables(serviceId: string): Promise<ServiceVariable[]> {
const payload = await requestJson<{ variables?: RawServiceVariable[] }>(`/services/${serviceId}/variables`);
return normalizeServiceVariables(payload.variables);
}
export type UpdateServiceVariableInput = components['schemas']['VariableInput'];
export async function updateServiceVariables(
serviceId: string,
variables: UpdateServiceVariableInput[],
): Promise<ServiceVariable[]> {
const payload = await requestJson<{ variables?: RawServiceVariable[] }>(
`/services/${serviceId}/variables`,
{ method: 'PUT', body: JSON.stringify({ variables }) },
);
return normalizeServiceVariables(payload.variables);
}
export async function listAuditLogs(input: ListAuditLogsInput = {}): Promise<AuditLogEntity[]> {
const searchParams = new URLSearchParams();
if (input.resource) {