mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 18:46:47 +09:00
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>
83 lines
3.1 KiB
JavaScript
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';
|
|
}
|