Centralize trimmed string normalization

This commit is contained in:
rcourtman
2026-03-19 09:39:25 +00:00
parent d126cec1f8
commit 9dbf343662
8 changed files with 69 additions and 80 deletions
@@ -14,6 +14,7 @@ import {
} from '@/components/shared/Table';
import { EmptyState } from '@/components/shared/EmptyState';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import {
formatSwarmClusterId,
formatSwarmClusterSummary,
@@ -77,8 +78,6 @@ type ResourcesListResponse = {
};
};
const normalize = (value?: string | null) => (value || '').trim();
const buildServicesUrl = (cluster: string, page: number) => {
const params = new URLSearchParams();
params.set('type', 'docker-service');
@@ -120,8 +119,8 @@ const fetchAllServices = async (cluster: string): Promise<DockerServiceResource[
};
const formatUpdate = (update?: DockerServiceUpdate) => {
const state = normalize(update?.state);
const message = normalize(update?.message);
const state = asTrimmedString(update?.state) ?? '';
const message = asTrimmedString(update?.message) ?? '';
if (!state && !message) return '—';
if (state && message) return `${state}: ${message}`;
return state || message;
@@ -133,7 +132,7 @@ const formatPorts = (ports?: DockerServicePort[]) => {
.map((p) => {
const target = typeof p.targetPort === 'number' ? String(p.targetPort) : '';
const published = typeof p.publishedPort === 'number' ? String(p.publishedPort) : '';
const proto = normalize(p.protocol) || 'tcp';
const proto = asTrimmedString(p.protocol) || 'tcp';
if (published && target) return `${published}->${target}/${proto}`;
if (target) return `${target}/${proto}`;
return '';
@@ -146,7 +145,7 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
const [search, setSearch] = createSignal('');
const drawerPresentation = getSwarmDrawerPresentation();
const clusterKey = createMemo(() => normalize(props.cluster));
const clusterKey = createMemo(() => asTrimmedString(props.cluster) ?? '');
const [services] = createResource(
clusterKey,
@@ -158,18 +157,18 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
);
const filteredServices = createMemo(() => {
const term = normalize(search()).toLowerCase();
const term = (asTrimmedString(search()) ?? '').toLowerCase();
return services()
.filter((svc) => {
if (!term) return true;
const name = normalize(svc.name) || svc.id;
const stack = normalize(svc.docker?.stack);
const image = normalize(svc.docker?.image);
const name = asTrimmedString(svc.name) || svc.id;
const stack = asTrimmedString(svc.docker?.stack) ?? '';
const image = asTrimmedString(svc.docker?.image) ?? '';
return [name, stack, image].some((value) => value.toLowerCase().includes(term));
})
.sort((a, b) => {
const aName = (normalize(a.name) || a.id).toLowerCase();
const bName = (normalize(b.name) || b.id).toLowerCase();
const aName = (asTrimmedString(a.name) || a.id).toLowerCase();
const bName = (asTrimmedString(b.name) || b.id).toLowerCase();
if (aName !== bName) return aName < bName ? -1 : 1;
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
});
@@ -177,9 +176,9 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
const swarm = createMemo(() => props.swarm);
const clusterName = createMemo(
() => normalize(swarm()?.clusterName) || normalize(swarm()?.clusterId) || clusterKey(),
() => asTrimmedString(swarm()?.clusterName) || asTrimmedString(swarm()?.clusterId) || clusterKey(),
);
const clusterId = createMemo(() => normalize(swarm()?.clusterId));
const clusterId = createMemo(() => asTrimmedString(swarm()?.clusterId) ?? '');
return (
<div class="space-y-3">
@@ -211,20 +210,20 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
<Show
when={
normalize(swarm()?.nodeRole) ||
normalize(swarm()?.localState) ||
asTrimmedString(swarm()?.nodeRole) ||
asTrimmedString(swarm()?.localState) ||
typeof swarm()?.controlAvailable === 'boolean'
}
>
<div class="mt-2 flex flex-wrap gap-2 text-[11px]">
<Show when={normalize(swarm()?.nodeRole)}>
<Show when={asTrimmedString(swarm()?.nodeRole)}>
<span class="inline-flex items-center rounded bg-surface-alt px-2 py-0.5 text-base-content">
{formatSwarmRoleLabel(normalize(swarm()?.nodeRole))}
{formatSwarmRoleLabel(asTrimmedString(swarm()?.nodeRole))}
</span>
</Show>
<Show when={normalize(swarm()?.localState)}>
<Show when={asTrimmedString(swarm()?.localState)}>
<span class="inline-flex items-center rounded bg-surface-alt px-2 py-0.5 text-base-content">
{formatSwarmStateLabel(normalize(swarm()?.localState))}
{formatSwarmStateLabel(asTrimmedString(swarm()?.localState))}
</span>
</Show>
<Show when={typeof swarm()?.controlAvailable === 'boolean'}>
@@ -235,9 +234,9 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
</div>
</Show>
<Show when={normalize(swarm()?.error)}>
<Show when={asTrimmedString(swarm()?.error)}>
<div class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1.5 text-[10px] text-amber-800 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-200">
{normalize(swarm()?.error)}
{asTrimmedString(swarm()?.error)}
</div>
</Show>
</Card>
@@ -287,10 +286,10 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
<TableBody class="divide-y divide-border-subtle">
<For each={filteredServices()}>
{(svc) => {
const name = () => normalize(svc.name) || svc.id;
const stack = () => normalize(svc.docker?.stack) || '—';
const image = () => normalize(svc.docker?.image) || '—';
const mode = () => normalize(svc.docker?.mode) || '—';
const name = () => asTrimmedString(svc.name) || svc.id;
const stack = () => asTrimmedString(svc.docker?.stack) || '—';
const image = () => asTrimmedString(svc.docker?.image) || '—';
const mode = () => asTrimmedString(svc.docker?.mode) || '—';
const desired = () => svc.docker?.desiredTasks ?? 0;
const running = () => svc.docker?.runningTasks ?? 0;
const update = () => formatUpdate(svc.docker?.serviceUpdate);
@@ -22,6 +22,7 @@ import {
getK8sDeploymentsLoadingState,
} from '@/utils/k8sDeploymentPresentation';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
const PAGE_LIMIT = 100;
const MAX_PAGES = 20;
@@ -46,8 +47,6 @@ type ResourcesListResponse = {
};
};
const normalize = (value?: string | null) => (value || '').trim();
const buildDeploymentsUrl = (cluster: string, page: number) => {
const params = new URLSearchParams();
params.set('type', 'k8s-deployment');
@@ -100,7 +99,7 @@ export const K8sDeploymentsDrawer: Component<{
const [lastAppliedNamespace, setLastAppliedNamespace] = createSignal('');
const drawerPresentation = getK8sDeploymentsDrawerPresentation();
const clusterName = createMemo(() => normalize(props.cluster));
const clusterName = createMemo(() => asTrimmedString(props.cluster) ?? '');
const [deployments] = createResource(
clusterName,
@@ -114,7 +113,7 @@ export const K8sDeploymentsDrawer: Component<{
const namespaceOptions = createMemo(() => {
const set = new Set<string>();
for (const dep of deployments()) {
const ns = normalize(dep.kubernetes?.namespace);
const ns = asTrimmedString(dep.kubernetes?.namespace) ?? '';
if (ns) set.add(ns);
}
return Array.from(set).sort((a, b) => a.localeCompare(b));
@@ -122,9 +121,10 @@ export const K8sDeploymentsDrawer: Component<{
createEffect(() => {
// Allow other drawer tabs (e.g., Namespaces) to prefill the namespace filter.
const next = normalize(props.initialNamespace);
const next = asTrimmedString(props.initialNamespace) ?? '';
if (!next) return;
if (next.toLowerCase() === normalize(lastAppliedNamespace()).toLowerCase()) return;
if (next.toLowerCase() === (asTrimmedString(lastAppliedNamespace()) ?? '').toLowerCase())
return;
const exists = namespaceOptions().some((ns) => ns.toLowerCase() === next.toLowerCase());
if (exists) {
setNamespace(next);
@@ -133,7 +133,7 @@ export const K8sDeploymentsDrawer: Component<{
});
createEffect(() => {
const current = normalize(namespace());
const current = asTrimmedString(namespace()) ?? '';
if (!current) return;
const exists = namespaceOptions().some((ns) => ns.toLowerCase() === current.toLowerCase());
if (!exists) {
@@ -142,19 +142,19 @@ export const K8sDeploymentsDrawer: Component<{
});
const filteredDeployments = createMemo(() => {
const ns = normalize(namespace());
const term = normalize(search()).toLowerCase();
const ns = asTrimmedString(namespace()) ?? '';
const term = (asTrimmedString(search()) ?? '').toLowerCase();
return deployments()
.filter((dep) => {
if (ns && normalize(dep.kubernetes?.namespace) !== ns) return false;
if (ns && (asTrimmedString(dep.kubernetes?.namespace) ?? '') !== ns) return false;
if (!term) return true;
const name = normalize(dep.name) || dep.id;
const name = asTrimmedString(dep.name) || dep.id;
return name.toLowerCase().includes(term);
})
.sort((a, b) => {
const aName = (normalize(a.name) || a.id).toLowerCase();
const bName = (normalize(b.name) || b.id).toLowerCase();
const aName = (asTrimmedString(a.name) || a.id).toLowerCase();
const bName = (asTrimmedString(b.name) || b.id).toLowerCase();
if (aName !== bName) return aName < bName ? -1 : 1;
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
});
@@ -167,7 +167,7 @@ export const K8sDeploymentsDrawer: Component<{
buildWorkloadsPath({
type: 'pod',
context: cluster,
namespace: normalize(ns) || null,
namespace: asTrimmedString(ns) || null,
}),
);
};
@@ -256,8 +256,8 @@ export const K8sDeploymentsDrawer: Component<{
<TableBody class="divide-y divide-border-subtle">
<For each={filteredDeployments()}>
{(dep) => {
const name = () => normalize(dep.name) || dep.id;
const ns = () => normalize(dep.kubernetes?.namespace) || '—';
const name = () => asTrimmedString(dep.name) || dep.id;
const ns = () => asTrimmedString(dep.kubernetes?.namespace) || '—';
const desired = () => dep.kubernetes?.desiredReplicas ?? 0;
const updated = () => dep.kubernetes?.updatedReplicas ?? 0;
const ready = () => dep.kubernetes?.readyReplicas ?? 0;
@@ -22,6 +22,7 @@ import {
getK8sNamespacesLoadingState,
} from '@/utils/k8sNamespacePresentation';
import { getNamespaceCountsIndicator, type NamespaceCounts } from '@/utils/k8sStatusPresentation';
import { asTrimmedString } from '@/utils/stringUtils';
type NamespaceRow = {
namespace: string;
@@ -34,8 +35,6 @@ type NamespacesResponse = {
data: NamespaceRow[];
};
const normalize = (value?: string | null) => (value || '').trim();
const formatInteger = (value?: number | null): string => {
const n = Number(value ?? 0);
if (!Number.isFinite(n)) return '0';
@@ -50,7 +49,7 @@ export const K8sNamespacesDrawer: Component<{
const [search, setSearch] = createSignal('');
const drawerPresentation = getK8sNamespacesDrawerPresentation();
const clusterName = createMemo(() => normalize(props.cluster));
const clusterName = createMemo(() => asTrimmedString(props.cluster) ?? '');
const [namespaces] = createResource(
clusterName,
@@ -75,7 +74,7 @@ export const K8sNamespacesDrawer: Component<{
const rows = createMemo(() => (Array.isArray(namespaces()?.data) ? namespaces()!.data : []));
const filteredRows = createMemo(() => {
const term = normalize(search()).toLowerCase();
const term = (asTrimmedString(search()) ?? '').toLowerCase();
if (!term) return rows();
return rows().filter((row) => row.namespace.toLowerCase().includes(term));
});
@@ -87,7 +86,7 @@ export const K8sNamespacesDrawer: Component<{
buildWorkloadsPath({
type: 'pod',
context: cluster,
namespace: namespace ? normalize(namespace) : null,
namespace: asTrimmedString(namespace) ?? null,
}),
);
};
@@ -14,6 +14,7 @@ import {
import { EmptyState } from '@/components/shared/EmptyState';
import { formatBytes, formatRelativeTime } from '@/utils/format';
import { getServiceHealthPresentation } from '@/utils/serviceHealthPresentation';
import { asTrimmedString } from '@/utils/stringUtils';
import {
getPMGDetailsDrawerPresentation,
PMG_DETAILS_FAILURE_STATE_TITLE,
@@ -121,8 +122,6 @@ type PMGInstanceDrawerProps = {
resourceName?: string;
};
const normalize = (value?: string | null) => (value || '').trim();
const formatCompact = (value?: number | null): string => {
if (value === undefined || value === null || Number.isNaN(value)) return '—';
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
@@ -135,7 +134,7 @@ export const PMGInstanceDrawer: Component<PMGInstanceDrawerProps> = (props) => {
const [searchDomain, setSearchDomain] = createSignal('');
const drawerPresentation = getPMGDetailsDrawerPresentation();
const resourceId = createMemo(() => normalize(props.resourceId));
const resourceId = createMemo(() => asTrimmedString(props.resourceId) ?? '');
const [resource] = createResource(
resourceId,
@@ -174,7 +173,7 @@ export const PMGInstanceDrawer: Component<PMGInstanceDrawerProps> = (props) => {
const relayDomains = createMemo(() => {
const rows = pmg()?.relayDomains ?? [];
const term = normalize(searchRelay()).toLowerCase();
const term = (asTrimmedString(searchRelay()) ?? '').toLowerCase();
if (!term) return rows;
return rows.filter(
(row) =>
@@ -184,7 +183,7 @@ export const PMGInstanceDrawer: Component<PMGInstanceDrawerProps> = (props) => {
const domainStats = createMemo(() => {
const rows = pmg()?.domainStats ?? [];
const term = normalize(searchDomain()).toLowerCase();
const term = (asTrimmedString(searchDomain()) ?? '').toLowerCase();
const filtered = term ? rows.filter((row) => row.domain.toLowerCase().includes(term)) : rows;
return [...filtered].sort((a, b) => (b.mailCount || 0) - (a.mailCount || 0));
});
@@ -192,7 +191,7 @@ export const PMGInstanceDrawer: Component<PMGInstanceDrawerProps> = (props) => {
const spamBuckets = createMemo(() => {
const rows = pmg()?.spamDistribution ?? [];
const parsed = rows
.map((row) => ({ bucket: normalize(row.bucket), count: Number(row.count || 0) }))
.map((row) => ({ bucket: asTrimmedString(row.bucket) ?? '', count: Number(row.count || 0) }))
.filter((row) => row.bucket.length > 0);
return parsed.sort((a, b) => a.bucket.localeCompare(b.bucket));
});
@@ -240,7 +239,7 @@ export const PMGInstanceDrawer: Component<PMGInstanceDrawerProps> = (props) => {
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<div class="text-sm font-semibold text-base-content truncate">
{normalize(props.resourceName) ||
{asTrimmedString(props.resourceName) ||
resource()?.name ||
drawerPresentation.defaultResourceName}
</div>
@@ -1917,7 +1917,7 @@ describe('frontend resource type boundaries', () => {
expect(resourceRelationshipPresentationSource).toContain('formatConfidencePercentage');
expect(resourceCorrelationPresentationSource).toContain('formatConfidencePercentage');
expect(resourceCorrelationPresentationSource).toContain('humanizeArrowDelimitedLabel');
expect(resourceCorrelationPresentationSource).toContain('formatTrimmedLabel');
expect(resourceCorrelationPresentationSource).toContain('asTrimmedString');
expect(confidencePresentationSource).toContain('formatConfidencePercentage');
expect(confidencePresentationSource).toContain('formatConfidenceLabel');
expect(approvalPresentationSource).toContain('getResourceApprovalLevelLabel');
@@ -1926,7 +1926,6 @@ describe('frontend resource type boundaries', () => {
expect(resourceChangePresentationSource).toContain('humanizeToken');
expect(resourceRelationshipPresentationSource).toContain('humanizeToken');
expect(textPresentationSource).toContain('humanizeArrowDelimitedLabel');
expect(textPresentationSource).toContain('formatTrimmedLabel');
expect(resourceCorrelationPresentationSource).not.toContain('formatResourceCorrelationEndpointLabel');
expect(resourceCorrelationPresentationSource).not.toContain(
"replace(/\\s*->\\s*/g, ' → ')",
@@ -1934,6 +1933,15 @@ describe('frontend resource type boundaries', () => {
expect(resourceDetailDrawerSource).toContain('humanizeToken');
expect(textPresentationSource).toContain('humanizeToken');
expect(textPresentationSource).toContain('formatIdentifierLabel');
expect(resourceCorrelationPresentationSource).not.toContain('formatTrimmedLabel');
expect(swarmServicesDrawerSource).toContain('asTrimmedString');
expect(swarmServicesDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();");
expect(pmgInstanceDrawerSource).toContain('asTrimmedString');
expect(pmgInstanceDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();");
expect(k8sNamespacesDrawerSource).toContain('asTrimmedString');
expect(k8sNamespacesDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();");
expect(k8sDeploymentsDrawerSource).toContain('asTrimmedString');
expect(k8sDeploymentsDrawerSource).not.toContain("const normalize = (value?: string | null) => (value || '').trim();");
expect(messageItemSource).toContain('formatIdentifierLabel');
expect(toolExecutionBlockSource).toContain('formatIdentifierLabel');
expect(aiChatSource).toContain('formatIdentifierLabel');
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
import {
formatIdentifierLabel,
formatTrimmedLabel,
humanizeArrowDelimitedLabel,
humanizeToken,
titleCaseDelimitedLabel,
@@ -32,11 +31,6 @@ describe('textPresentation', () => {
expect(formatIdentifierLabel(undefined, { fallback: 'Unknown' })).toBe('Unknown');
});
it('formats trimmed labels with a fallback', () => {
expect(formatTrimmedLabel(' endpoint ', { fallback: 'Unknown resource' })).toBe('endpoint');
expect(formatTrimmedLabel('', { fallback: 'Unknown resource' })).toBe('Unknown resource');
});
it('title-cases delimited labels with configurable separators', () => {
expect(titleCaseDelimitedLabel('docker_host')).toBe('Docker Host');
expect(titleCaseDelimitedLabel('IP_address', { preserveShortAllCaps: true })).toBe(
@@ -1,7 +1,8 @@
import type { ResourceCorrelation } from '@/types/aiIntelligence';
import { formatDurationMs } from '@/utils/patrolFormat';
import { formatConfidencePercentage } from '@/utils/confidencePresentation';
import { formatTrimmedLabel, humanizeArrowDelimitedLabel } from '@/utils/textPresentation';
import { asTrimmedString } from '@/utils/stringUtils';
import { humanizeArrowDelimitedLabel } from '@/utils/textPresentation';
const parseGoDurationMs = (value: string): number | null => {
const normalized = value.trim();
@@ -35,11 +36,12 @@ export function formatResourceCorrelationEndpoint(
correlation: ResourceCorrelation,
role: 'source' | 'target',
): string {
return formatTrimmedLabel(
role === 'source'
? correlation.source_name || correlation.source_id
: correlation.target_name || correlation.target_id,
{ fallback: 'Unknown resource' },
return (
asTrimmedString(
role === 'source'
? correlation.source_name || correlation.source_id
: correlation.target_name || correlation.target_id,
) || 'Unknown resource'
);
}
@@ -15,10 +15,6 @@ export interface TitleCaseLabelOptions {
separators?: RegExp;
}
export interface TrimmedLabelOptions {
fallback?: string;
}
export function humanizeToken(value?: string, options?: HumanizeTokenOptions): string {
const normalized = (value || '').trim();
if (!normalized) {
@@ -87,14 +83,6 @@ export function titleCaseDelimitedLabel(
.join(' ');
}
export function formatTrimmedLabel(
value?: string,
options?: TrimmedLabelOptions,
): string {
const normalized = (value || '').trim();
return normalized || options?.fallback || '';
}
export function humanizeArrowDelimitedLabel(
value?: string,
options?: HumanizeTokenOptions,