Merge published Kubernetes job time presentation

Preserve published source and exact browser receipts; resolve only the shared receipt pointer.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-02 22:55:01 +01:00
4 changed files with 147 additions and 59 deletions
@@ -790,6 +790,13 @@ dense platform table rendering must use `PlatformTableRelativeTimeValue` /
`formatPlatformTableRelativeTimeValue` for relative labels, compact defaults,
invalid/empty markers, and tabular styling instead of importing
`formatRelativeTime` or declaring local timestamp-age helpers in table files.
Kubernetes controller Detail timestamps (Job completion and start, CronJob
last success and last schedule) are relative-age values under that rule:
`KubernetesControllersTable` renders them as a labelled age through
`formatPlatformTableRelativeTimeValue` with the absolute
`formatPlatformTableDateTimeValue` time as hover title, never as the raw RFC
3339 string the agent reports, and the Detail column carries enough weight
for those labelled ages to fit whole on a full-width desktop table.
Duration and interval cells keep the same split: unified-resource or
source-specific consumers own which elapsed duration, human fallback, or poll
interval field is meaningful, while dense platform table rendering must use
+23 -35
View File
@@ -1,57 +1,45 @@
{
"version": 1,
"base_sha": "e5d0843851a3837a90d71afaced3a009c0606bbf",
"verified_at": "2026-10-02T20:44:41.220410Z",
"base_sha": "e6540da45da4de4bcb954463373d8ea9f196eb56",
"verified_at": "2026-10-02T21:04:53Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/HistoryChart.tsx",
"frontend-modern/src/components/shared/useHistoryChartState.ts"
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/HistoryChart.tsx": "74fb7e6b466e1c2b2d2efb13e0a124d6c301896a7c9423432e903908cc9ee25b",
"frontend-modern/src/components/shared/useHistoryChartState.ts": "d6e8317019c8dc616bd9a4d3c6c71cb5329297ca0341e6ba28a6cc4c84db36b0"
"frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "f2d8a72d62d06a89d3a70bd82d63d658ece3ac8acf3a0f688fb751a250d6f057"
},
"routes": [
"/browser-tests/history-touch.html"
"/kubernetes"
],
"viewports": [
{
"width": 1365,
"width": 1440,
"height": 900
},
{
"width": 1280,
"height": 900
},
{
"width": 900,
"height": 900
},
{
"width": 390,
"height": 844
"height": 900
}
],
"states": [
"Production disk/pool details with stored multi-point, measured-zero singleton and empty history; 19 observed presentations across desktop Chromium and phone Chromium/WebKit",
"Actual touchscreen first/middle/last taps, shared I/O group focus transfer, keyboard navigation/exit and real mouse coexistence; phone WebKit dark-class presentation"
"Mock estate (1 Kubernetes cluster, 2 nodes, 6 pods, 2 deployments) with one CronJob carrying lastSuccessfulTime, one completed Job, one DaemonSet and one StatefulSet in the Workload Controllers table, Chromium",
"Workload Controllers table at the full desktop layout where the Detail column is shown (1440px) and at container widths where it is hidden (1280, 900, 390px)",
"Controller row expanded to its detail disclosure at 390px"
],
"interactions": [
"Native touchscreen taps select the actual reading at that position instead of the latest sample; touch compatibility focus/mouse movement/leave cannot overwrite it",
"Primary contact cancellation, non-tap gesture rejection and native Chromium vertical pan with room-to-scroll precondition; no prevented touch default, pointer capture or touch-action change",
"Desktop/phone keyboard Home/Right/End/Escape and focus exit, pointer announcements remain quiet, range replacement, zero/empty responses and no horizontal overflow",
"Predecessor native Chromium/WebKit first tap near 10% instead displays 30%; final first/middle/last taps display 10/20/30%. Inspected predecessor and final phone, zero/empty and desktop screenshots"
"Measured every controllers table header and cell for clipped text (scrollWidth over clientWidth) at each viewport; before the change the Detail cells needed 258px in a 119px column, after it the CronJob row reads \"Last success 1h ago\" and the Job row \"Completed 41m ago\" with no clipped element in the column and no horizontal page scroll",
"Read the Detail span title attributes: they carry the absolute local time (\"Last success: Oct 2, 2026, 08:51 PM\") instead of the raw RFC 3339 string",
"Probed the rendered width of every label the column can produce (Started, Completed, Last success, Last run with ages up to 365d) against the 135px Detail cell at 1440px; all fit whole",
"Compared before and after screenshots of the table at 1440px; the StatefulSet service name now truncates with an ellipsis inside the cell instead of being cut by the cell edge"
],
"command": "pulse-worker-browser frontend-modern/browser-tests/history-touch.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
"browser/result.json",
"browser/chromium-phone-first.png",
"browser/webkit-phone-first.png",
"browser/webkit-phone-middle.png",
"browser/webkit-phone-pool-first.png",
"browser/webkit-phone-pool-zero-singleton.png",
"browser/webkit-phone-pool-empty.png",
"browser/chromium-desktop-middle.png",
"browser/parent.json",
"browser/parent-chromium-phone-first.png",
"browser/parent-webkit-phone-first.png"
],
"limitations": "Synthetic stored APIs, not native PBS/TrueNAS collection, physical phones or assistive-device speech. Native pan is Chromium-only; no physical/WebKit pan, pinch-zoom or whole-app theme-transition claim. Initial fixture and pan-driver failures are retained separately."
"command": "playwright chromium against a private mock-mode stack built from this commit (backend :7693, vite :5213)"
}
@@ -13,6 +13,8 @@ import {
PlatformTableToolbar,
createPlatformTableFilterState,
createPlatformTableSortState,
formatPlatformTableDateTimeValue,
formatPlatformTableRelativeTimeValue,
formatPlatformTableTextValue,
getPlatformTableCellClassForKind,
PlatformTableShell,
@@ -120,36 +122,60 @@ const exceptionSummary = (resource: Resource): string => {
}
};
const apiDetail = (resource: Resource): string => {
type ControllerDetail = { text: string; title: string };
const plainDetail = (text: string): ControllerDetail => ({ text, title: text });
// Kubernetes reports job timestamps as RFC 3339 strings with microseconds,
// which never fit the Detail column. The cell says how long ago the event
// happened and keeps the absolute time on hover.
const timeDetail = (label: string, timestamp: string): ControllerDetail => {
const relative = formatPlatformTableRelativeTimeValue(timestamp, { emptyText: '' });
if (!relative) return plainDetail(`${label}: ${timestamp}`);
const absolute = formatPlatformTableDateTimeValue(timestamp, {
dateTimeFormat: { year: 'numeric' },
});
return { text: `${label} ${relative}`, title: `${label}: ${absolute}` };
};
const apiDetail = (resource: Resource): ControllerDetail => {
switch (resource.type) {
case 'k8s-replicaset': {
if (typeof resource.kubernetes?.fullyLabeledReplicas === 'number') {
return `Fully labeled: ${resource.kubernetes.fullyLabeledReplicas}`;
return plainDetail(`Fully labeled: ${resource.kubernetes.fullyLabeledReplicas}`);
}
return typeof resource.kubernetes?.observedGeneration === 'number'
? `Observed: ${resource.kubernetes.observedGeneration}`
: '—';
return plainDetail(
typeof resource.kubernetes?.observedGeneration === 'number'
? `Observed: ${resource.kubernetes.observedGeneration}`
: '—',
);
}
case 'k8s-statefulset':
return resource.kubernetes?.serviceName ? `Service: ${resource.kubernetes.serviceName}` : '—';
return plainDetail(
resource.kubernetes?.serviceName ? `Service: ${resource.kubernetes.serviceName}` : '—',
);
case 'k8s-daemonset':
return typeof resource.kubernetes?.updatedReplicas === 'number'
? `Updated: ${resource.kubernetes.updatedReplicas}`
: '—';
return plainDetail(
typeof resource.kubernetes?.updatedReplicas === 'number'
? `Updated: ${resource.kubernetes.updatedReplicas}`
: '—',
);
case 'k8s-job':
if (resource.kubernetes?.completionTime) {
return `Completed: ${resource.kubernetes.completionTime}`;
return timeDetail('Completed', resource.kubernetes.completionTime);
}
return resource.kubernetes?.startTime ? `Started: ${resource.kubernetes.startTime}` : '—';
return resource.kubernetes?.startTime
? timeDetail('Started', resource.kubernetes.startTime)
: plainDetail('—');
case 'k8s-cronjob':
if (resource.kubernetes?.lastSuccessfulTime) {
return `Last success: ${resource.kubernetes.lastSuccessfulTime}`;
return timeDetail('Last success', resource.kubernetes.lastSuccessfulTime);
}
return resource.kubernetes?.lastScheduleTime
? `Last schedule: ${resource.kubernetes.lastScheduleTime}`
: '—';
? timeDetail('Last run', resource.kubernetes.lastScheduleTime)
: plainDetail('—');
default:
return '—';
return plainDetail('—');
}
};
@@ -291,7 +317,7 @@ export const KubernetesControllersTable: Component<{
kind="text"
sort={sort}
sortKey="kind"
class="platform-table-mobile-w-15 md:w-[9%]"
class="platform-table-mobile-w-15 md:w-[8%]"
>
Kind
</PlatformSortableTableHead>
@@ -315,7 +341,7 @@ export const KubernetesControllersTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="current"
class="hidden sm:table-cell md:w-[8%]"
class="hidden sm:table-cell md:w-[7%]"
>
Current
</PlatformSortableTableHead>
@@ -331,7 +357,7 @@ export const KubernetesControllersTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="available"
class="hidden md:table-cell md:w-[9%]"
class="hidden md:table-cell md:w-[8%]"
>
Available
</PlatformSortableTableHead>
@@ -346,7 +372,7 @@ export const KubernetesControllersTable: Component<{
<PlatformSortableTableHead
kind="text"
sort={sort}
class="hidden lg:table-cell md:w-[9%]"
class="hidden lg:table-cell md:w-[12%]"
>
Detail
</PlatformSortableTableHead>
@@ -436,8 +462,8 @@ export const KubernetesControllersTable: Component<{
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
>
<span class="inline-block max-w-[14rem] truncate" title={detail()}>
{detail()}
<span class="block truncate" title={detail().title}>
{detail().text}
</span>
</TableCell>
</TableRow>
@@ -1,6 +1,7 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { formatPlatformTableDateTimeValue } from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import { KubernetesControllersTable } from '../KubernetesControllersTable';
@@ -24,10 +25,17 @@ const makeResource = ({
afterEach(() => {
cleanup();
vi.useRealTimers();
});
const absoluteTime = (timestamp: string): string =>
formatPlatformTableDateTimeValue(timestamp, { dateTimeFormat: { year: 'numeric' } });
describe('KubernetesControllersTable', () => {
it('renders native controller fields for ReplicaSet, StatefulSet, DaemonSet, Job, and CronJob rows', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-24T13:31:00Z'));
render(() => (
<KubernetesControllersTable
resources={[
@@ -133,11 +141,70 @@ describe('KubernetesControllersTable', () => {
expect(screen.getByText('Job')).toBeInTheDocument();
expect(screen.getByText('10 completions')).toBeInTheDocument();
expect(screen.getByText('Failed: 2')).toBeInTheDocument();
expect(screen.getByText('Completed: 2026-05-24T13:00:00Z')).toBeInTheDocument();
// Job and CronJob timestamps arrive as RFC 3339 strings; the cell shows
// how long ago and keeps the absolute time on hover.
const completed = screen.getByText('Completed 31m ago');
expect(completed).toHaveAttribute(
'title',
`Completed: ${absoluteTime('2026-05-24T13:00:00Z')}`,
);
expect(screen.getByText('CronJob')).toBeInTheDocument();
expect(screen.getByText('*/5 * * * *')).toBeInTheDocument();
expect(screen.getByText('Suspended')).toBeInTheDocument();
expect(screen.getByText('Last success: 2026-05-24T12:55:00Z')).toBeInTheDocument();
const lastSuccess = screen.getByText('Last success 36m ago');
expect(lastSuccess).toHaveAttribute(
'title',
`Last success: ${absoluteTime('2026-05-24T12:55:00Z')}`,
);
expect(screen.queryByText(/2026-05-24T1[23]/)).not.toBeInTheDocument();
});
it('falls back to start and last-run times when a Job or CronJob has not completed', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-24T13:31:00Z'));
render(() => (
<KubernetesControllersTable
resources={[
makeResource({
id: 'nightly-import',
type: 'k8s-job',
kubernetes: {
clusterName: 'prod',
namespace: 'batch',
resourceKind: 'Job',
desiredReplicas: 1,
active: 1,
startTime: '2026-05-24T13:29:30Z',
},
}),
makeResource({
id: 'billing-rollup',
type: 'k8s-cronjob',
kubernetes: {
clusterName: 'prod',
namespace: 'batch',
resourceKind: 'CronJob',
schedule: '0 2 * * *',
lastScheduleTime: '2026-05-22T02:00:00Z',
},
}),
]}
emptyIcon={<span />}
emptyTitle="No controllers"
emptyDescription="No controllers"
showToolbar={false}
/>
));
expect(screen.getByText('Started 1m ago')).toHaveAttribute(
'title',
`Started: ${absoluteTime('2026-05-24T13:29:30Z')}`,
);
expect(screen.getByText('Last run 2d ago')).toHaveAttribute(
'title',
`Last run: ${absoluteTime('2026-05-22T02:00:00Z')}`,
);
});
});