Files
ServerKit/frontend/src/utils/deployActivity.js
T
Juan DenisandClaude Opus 5 258b125532 feat(ui): the last hand-rolled list surfaces join the convention
Docker's four remaining tabs (Compose, Images, Volumes, Networks) drop their raw
<table className="docker-table"> markup and bespoke toolbars for the shape
ContainersTab already used. Each gets a distinct viewPageKey AND urlScope, since
one route now mounts five tables and a shared scope has them writing each other's
?view=. Views were chosen from what the payload actually carries rather than what
would read well: Volumes has no "Unused" view because neither transport reports a
reference count, and a Networks "Containers" column was dropped because the agent
sends the count and the local service does not — a column that is permanently
blank on half your hosts is the exact failure this round exists to stop.

Terminal stops hand-rolling two surfaces that already had converged twins in the
repo, imported by nothing but the Style Guide: Processes renders ProcessTable,
and Services renders serverdetail/ServicesTab, taught to serve the local host as
well as a paired agent. This was mostly deletion.

Databases: the explorer's tables move onto the shared chrome, losing three raw
search inputs, two page-level row counts and their segmented rows.
Deployments: the fake table (a header row over <button> rows) becomes a real one,
and the target-server <select> becomes the Target column's own menu.
Templates keeps its card grid — a catalog is a legitimate place for tiles — but
loses the row count, filter-chip row and sort control the shared chrome replaces.
FTP and WorkspaceDetail get chrome on tables that had none.
components/SearchFilterBar.jsx is deleted: a complete competing search/filter/sort
bar with zero importers, confirmed not on the SDK export list.

Fixes for regressions the verification pass caught, before they shipped:
- Terminal > Processes swapped the whole table out on every `loading` tick. It
  auto-refreshes on a timer, so the chrome, active view, column rules, sort and
  scroll position were being unmounted and rebuilt every few seconds. Only the
  first load swaps now.
- The Databases backups pane returned its empty state INSTEAD of the chrome, so
  a tab with no backups had no Refresh — exactly when you want one, because a
  backup that just finished is why the list looks empty.
- Every open table tab kept its chrome mounted while sharing one urlScope, so
  four open tables fought over ?dbstruct.view=. Scoped per tab. The process list
  is namespaced per ENGINE too: `state` reads p.command on MySQL and p.state on
  PostgreSQL, so one namespace let a MySQL view apply itself to Postgres and
  match nothing.
- The Images size column disagreed with itself: the rule value was base-1000 MB
  while the cell rendered base-1024, so "Size is over 400 MB" matched a row whose
  own cell read "392.9 MB".
- Deployments' server filter used to reach the whole history through ?server_id=;
  as a column rule it only reaches what was fetched. The window is explicit now —
  a full page offers Load more instead of quietly filtering a truncated list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 02:16:47 -04:00

83 lines
3.1 KiB
JavaScript

// Shaping helpers for the Deploy Activity feed (/deployments) and the deploy
// console header. The list endpoint returns a flat job row — no per-step array —
// so the pipeline tick states are derived here from current_step/total_steps/
// status, keeping the feed a single cheap request.
export const KIND_CHIP = {
app_deploy: 'app',
template_install: 'template',
demo_deploy: 'test',
};
export const KIND_LABELS = {
app_deploy: 'App deploy',
template_install: 'Template install',
demo_deploy: 'Test (simulated)',
};
// One tick per pipeline step, coloured by how far the run got.
export function stepTicks(job) {
const total = job.total_steps || 0;
const current = job.current_step || 0;
const ticks = [];
for (let i = 1; i <= total; i += 1) {
let state = 'pending';
if (job.status === 'succeeded') state = 'done';
else if (i < current) state = 'done';
else if (i === current) {
if (job.status === 'failed') state = 'failed';
else if (job.status === 'running') state = 'running';
else state = 'done';
}
ticks.push(state);
}
return ticks;
}
// "main@3f8c1d2", an image tag, or the template ref — whatever identifies what
// was actually deployed. Null when nothing does (a simulated run, say), so
// callers can choose between a fallback and omitting the field entirely.
export function sourceRef(job) {
if (job.commit_hash) {
const short = job.commit_hash.slice(0, 7);
const branch = job.branch || job.result?.branch;
return branch ? `${branch}@${short}` : short;
}
if (job.image_tag) return job.image_tag;
if (job.kind === 'template_install') return job.result?.template || job.app_name || 'template';
return null;
}
export function formatDuration(seconds) {
if (seconds == null) return '—';
if (seconds < 10) return `${seconds.toFixed(1)}s`;
if (seconds < 60) return `${Math.round(seconds)}s`;
const m = Math.floor(seconds / 60);
const s = Math.round(seconds % 60);
return s ? `${m}m ${s}s` : `${m}m`;
}
export function relativeTime(iso, now = Date.now()) {
if (!iso) return '—';
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return '—';
const mins = (now - t) / 60000;
if (mins < 1) return 'just now';
if (mins < 60) return `${Math.floor(mins)}m ago`;
if (mins < 1440) return `${Math.floor(mins / 60)}h ago`;
return `${Math.floor(mins / 1440)}d ago`;
}
// Day bucket for a run, so the table can group chronologically without printing
// a date on every row. Queued runs are their own bucket — they have no start
// time to place them by, and they are the ones worth reading first.
//
// The hand-rolled orderer that used to sit here went with the feed: the table's
// own grouping sorts the buckets by the active sort, which is the same answer
// without a second copy of the ordering rules.
export function activityGroup(job, now = Date.now()) {
if (job.status === 'pending' || !job.started_at) return 'Queued';
const mins = (now - new Date(job.started_at).getTime()) / 60000;
return mins < 1440 ? 'Today' : 'Earlier';
}