1.0.33
This commit is contained in:
85 files changed
+10118
-1046
No files matched your search
@@ -0,0 +1,45 @@
|
||||
export type ChangelogToken =
|
||||
| { kind: 'text'; value: string }
|
||||
| { kind: 'issue'; num: number }
|
||||
| { kind: 'pr'; num: number }
|
||||
| { kind: 'user'; name: string };
|
||||
|
||||
const PATTERN = /PR#(\d+)|#(\d+)|@([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38}))/g;
|
||||
|
||||
export function parseChangelogTokens(text: string): ChangelogToken[] {
|
||||
const tokens: ChangelogToken[] = [];
|
||||
let lastIndex = 0;
|
||||
for (const match of text.matchAll(PATTERN)) {
|
||||
const start = match.index ?? 0;
|
||||
if (start > lastIndex) {
|
||||
tokens.push({ kind: 'text', value: text.slice(lastIndex, start) });
|
||||
}
|
||||
if (match[1]) {
|
||||
tokens.push({ kind: 'pr', num: Number(match[1]) });
|
||||
} else if (match[2]) {
|
||||
tokens.push({ kind: 'issue', num: Number(match[2]) });
|
||||
} else if (match[3]) {
|
||||
tokens.push({ kind: 'user', name: match[3] });
|
||||
}
|
||||
lastIndex = start + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
tokens.push({ kind: 'text', value: text.slice(lastIndex) });
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export const GITHUB_REPO = 'Finsys/dockhand';
|
||||
|
||||
export function tokenHref(token: ChangelogToken): string | null {
|
||||
switch (token.kind) {
|
||||
case 'issue':
|
||||
return `https://github.com/${GITHUB_REPO}/issues/${token.num}`;
|
||||
case 'pr':
|
||||
return `https://github.com/${GITHUB_REPO}/pull/${token.num}`;
|
||||
case 'user':
|
||||
return `https://github.com/${token.name}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Container resource-string parsers, shared between the create modal, the
|
||||
* edit modal, and the in-place update Apply buttons in ContainerSettingsTab.
|
||||
*
|
||||
* Two of these previously lived in CreateContainerModal AND EditContainerModal
|
||||
* as identical copies — moved here to prevent the drift that started when one
|
||||
* gained `tb` support and the other didn't.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a memory string like "512m", "1g", "2.5gb" into bytes.
|
||||
* Bare numbers are treated as bytes. Returns undefined for empty/garbage input.
|
||||
*
|
||||
* Units: k/kb, m/mb, g/gb, t/tb — base 1024 (binary), matching Docker's CLI.
|
||||
*/
|
||||
export function parseMemory(value: string | number | null | undefined): number | undefined {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const match = trimmed.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([kmgt]?b?)?$/);
|
||||
if (!match) return undefined;
|
||||
const num = parseFloat(match[1]);
|
||||
const unit = match[2] || '';
|
||||
switch (unit) {
|
||||
case 'k': case 'kb': return Math.floor(num * 1024);
|
||||
case 'm': case 'mb': return Math.floor(num * 1024 * 1024);
|
||||
case 'g': case 'gb': return Math.floor(num * 1024 * 1024 * 1024);
|
||||
case 't': case 'tb': return Math.floor(num * 1024 * 1024 * 1024 * 1024);
|
||||
default: return Math.floor(num);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CPU-limit string like "0.5", "1.5", "2" into NanoCpus (1e9 = 1 CPU).
|
||||
* Returns undefined for empty/garbage input.
|
||||
*/
|
||||
export function parseNanoCpus(value: string | number | null | undefined): number | undefined {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const num = parseFloat(trimmed);
|
||||
if (isNaN(num) || num <= 0) return undefined;
|
||||
return Math.floor(num * 1e9);
|
||||
}
|
||||
|
||||
/** Parse a bare positive integer string. Returns undefined for empty/garbage. */
|
||||
export function parsePositiveInt(value: string | number | null | undefined): number | undefined {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === 'number') return value > 0 ? Math.floor(value) : undefined;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
const num = parseInt(trimmed, 10);
|
||||
if (isNaN(num) || num <= 0) return undefined;
|
||||
return num;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Environment-name validation (#1179).
|
||||
*
|
||||
* Environment names end up in filesystem paths (e.g. $DATA_DIR/stacks/<name>/...
|
||||
* in stacks.ts:344) and occasionally in shell-formatted strings, so the name
|
||||
* has to be quote-safe and glob-free.
|
||||
*
|
||||
* The rule is permissive on purpose — strict alphanumeric would orphan
|
||||
* existing real-world env names like "rambo (ARM)" or "docker-websites".
|
||||
* Anything currently in the wild passes; only NEW characters that actively
|
||||
* cause breakage are rejected.
|
||||
*/
|
||||
|
||||
// Allowed characters: ASCII letters/digits + _ . - ( ) space + @
|
||||
// First char: any allowed except space (no leading whitespace).
|
||||
// Last char: any allowed except space and dot (no trailing whitespace,
|
||||
// no trailing dot to avoid Windows-style "hidden" trailing-dot issues).
|
||||
// Length: 1..64.
|
||||
export const ENV_NAME_RE = /^(?! )[A-Za-z0-9_.\-() +@](?:[A-Za-z0-9_.\-() +@]{0,62}[A-Za-z0-9_\-)+@])?$/;
|
||||
|
||||
export const ENV_NAME_MAX_LENGTH = 64;
|
||||
|
||||
export interface ValidationResult {
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an environment name. Returns a structured result so callers can
|
||||
* decide how to surface the error (server: 400 body, client: inline message).
|
||||
*/
|
||||
export function validateEnvName(name: unknown): ValidationResult {
|
||||
if (typeof name !== 'string') return { ok: false, reason: 'Name is required' };
|
||||
if (name.length === 0) return { ok: false, reason: 'Name is required' };
|
||||
if (name.length > ENV_NAME_MAX_LENGTH) {
|
||||
return { ok: false, reason: `Name must be ${ENV_NAME_MAX_LENGTH} characters or fewer` };
|
||||
}
|
||||
if (!ENV_NAME_RE.test(name)) {
|
||||
return {
|
||||
ok: false,
|
||||
reason:
|
||||
'Name may contain letters, digits, spaces, and any of - _ . ( ) + @ (no leading/trailing whitespace, no trailing dot, no slashes, no wildcards)'
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** Convenience boolean variant. */
|
||||
export function isValidEnvName(name: unknown): boolean {
|
||||
return validateEnvName(name).ok;
|
||||
}
|
||||
Reference in new issue
Block a user