Files
Containr/app/frontend/src/features/service/variable-utils.ts
T
Tomas DvorakandDevin 1d5b99e1ba 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>
2026-09-17 10:49:45 +02:00

49 lines
1.5 KiB
TypeScript

export type VariableDraft = { key: string; value: string; isSecret: boolean };
export const VARIABLE_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
// parseDotenv handles KEY=VAL lines, '#' comments, 'export ' prefixes and
// single/double-quoted values — enough for standard .env pastes.
export function parseDotenv(text: string): VariableDraft[] {
const rows: VariableDraft[] = [];
for (const rawLine of text.split('\n')) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
continue;
}
const stripped = line.startsWith('export ') ? line.slice(7).trim() : line;
const eq = stripped.indexOf('=');
if (eq <= 0) {
continue;
}
const key = stripped.slice(0, eq).trim();
let value = stripped.slice(eq + 1).trim();
if (
value.length >= 2 &&
((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
) {
value = value.slice(1, -1);
}
rows.push({ key, value, isSecret: false });
}
return rows;
}
export function validateVariableRows(rows: VariableDraft[]): Record<number, string> {
const errors: Record<number, string> = {};
const seen = new Set<string>();
rows.forEach((row, i) => {
const key = row.key.trim();
if (!key) {
errors[i] = 'Key is required';
} else if (!VARIABLE_KEY_RE.test(key)) {
errors[i] = 'Letters, digits, underscore; must not start with a digit';
} else if (seen.has(key)) {
errors[i] = 'Duplicate key';
} else {
seen.add(key);
}
});
return errors;
}