Merge claude/2
This commit is contained in:
commit
39ccb08119
@ -84,10 +84,52 @@
|
||||
.sys-images-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 14px;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
margin: 10px 0 6px;
|
||||
}
|
||||
.sys-images-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
/* Storage → Images: All / In use / Unused segmented filter. */
|
||||
.sys-img-filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
background: rgba(var(--text-rgb), 0.05);
|
||||
border: 1px solid rgba(var(--text-rgb), 0.10);
|
||||
border-radius: 9px;
|
||||
}
|
||||
.sys-img-filter-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
.sys-img-filter-btn:hover { color: var(--text-primary); }
|
||||
.sys-img-filter-btn.is-active {
|
||||
background: rgba(var(--accent-rgb), 0.16);
|
||||
color: var(--accent);
|
||||
}
|
||||
.sys-img-filter-n {
|
||||
font-size: 10px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.sys-img-filter-btn.is-active .sys-img-filter-n { opacity: 0.9; }
|
||||
.sys-images-list { margin-top: 4px; }
|
||||
.sys-image-item .task-header { cursor: default; }
|
||||
.sys-img-icon { color: rgba(var(--text-rgb), 0.6); flex-shrink: 0; }
|
||||
|
||||
@ -24,6 +24,7 @@ class SystemStoragePage {
|
||||
this._timer = null;
|
||||
this._expanded = new Set(); // app names whose folder breakdown is open
|
||||
this._selectedImageIds = new Set(); // images ticked for bulk delete
|
||||
this._imageFilter = 'all'; // images list filter: all | inuse | unused
|
||||
this._deleting = false; // a remove-images task is in flight
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onChange = this._onChange.bind(this);
|
||||
@ -67,12 +68,25 @@ class SystemStoragePage {
|
||||
// Remove a single image.
|
||||
const del = e.target.closest('[data-image-delete]');
|
||||
if (del) { this._deleteImages([del.getAttribute('data-image-delete')]); return; }
|
||||
// Clear All / Delete Selected.
|
||||
// Filter chip (All / In use / Unused) — re-render the list in place,
|
||||
// dropping any selection that belongs to the now-hidden group.
|
||||
const filt = e.target.closest('[data-image-filter]');
|
||||
if (filt) {
|
||||
const f = filt.getAttribute('data-image-filter');
|
||||
if (f && f !== this._imageFilter) {
|
||||
this._imageFilter = f;
|
||||
this._selectedImageIds.clear();
|
||||
this._rerenderImages();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Clear All / Delete Selected. With no explicit selection, act on the
|
||||
// rows currently shown (i.e. honour the active filter).
|
||||
const clr = e.target.closest('[data-images-clear]');
|
||||
if (clr) {
|
||||
const ids = this._selectedImageIds.size
|
||||
? [...this._selectedImageIds]
|
||||
: ((this.data && this.data.image_list) || []).map(im => im.id);
|
||||
: this._visibleImages().map(im => im.id);
|
||||
this._deleteImages(ids);
|
||||
return;
|
||||
}
|
||||
@ -354,7 +368,7 @@ class SystemStoragePage {
|
||||
dockerSection = `
|
||||
<div class="sys-section-head"><h2>Docker engine</h2><span class="sys-chart-meta">${fmt.bytes(total)} · ${fmt.bytes(recl)} reclaimable — the daemon's own usage, separate from your app data</span></div>
|
||||
<div class="sys-storage-cards">${cards}</div>
|
||||
${this._renderImages(images)}`;
|
||||
<div id="sys-images-region">${this._renderImages(images)}</div>`;
|
||||
}
|
||||
|
||||
body.innerHTML = `${headline}${appsSection}${dockerSection}`;
|
||||
@ -410,6 +424,41 @@ class SystemStoragePage {
|
||||
return `<svg class="sys-img-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>`;
|
||||
}
|
||||
|
||||
// True when an image is backing at least one container.
|
||||
_imageInUse(im) { return (im && im.containers > 0); }
|
||||
|
||||
// In-use images float to the top. Array#sort is stable, so within each
|
||||
// group the backend's largest-first order is preserved.
|
||||
_sortImages(images) {
|
||||
return images.slice().sort((a, b) =>
|
||||
(this._imageInUse(a) ? 0 : 1) - (this._imageInUse(b) ? 0 : 1));
|
||||
}
|
||||
|
||||
// Does an image pass the active filter chip? "unused" means "not in use" —
|
||||
// tagged-but-idle images and dangling <none> layers alike.
|
||||
_imageMatchesFilter(im) {
|
||||
if (this._imageFilter === 'inuse') return this._imageInUse(im);
|
||||
if (this._imageFilter === 'unused') return !this._imageInUse(im);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The rows actually on screen (sorted + filtered) — what Select all and a
|
||||
// selection-less Clear All operate on.
|
||||
_visibleImages() {
|
||||
const all = Array.isArray(this.data && this.data.image_list) ? this.data.image_list : [];
|
||||
return this._sortImages(all).filter(im => this._imageMatchesFilter(im));
|
||||
}
|
||||
|
||||
// Re-render just the images region in place (on a filter change), leaving
|
||||
// the donut/cards above untouched.
|
||||
_rerenderImages() {
|
||||
const region = this.root()?.querySelector('#sys-images-region');
|
||||
if (!region) { this._render(); return; }
|
||||
const images = Array.isArray(this.data && this.data.image_list) ? this.data.image_list : [];
|
||||
region.innerHTML = this._renderImages(images);
|
||||
this._updateImageSelectionUI();
|
||||
}
|
||||
|
||||
_renderImages(images) {
|
||||
const fmt = window.SystemFmt;
|
||||
const trash = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
|
||||
@ -418,24 +467,49 @@ class SystemStoragePage {
|
||||
return `<div class="sys-section-head"><h2>Images</h2></div><div class="sys-storage-app-empty">No images on disk.</div>`;
|
||||
}
|
||||
|
||||
// Clear All / Select all live in the section head (where the count used
|
||||
// to be), so the list itself is just the rows in a dark container.
|
||||
// Filter chips with a live count each; "all" leads with in-use images.
|
||||
const inUse = images.reduce((n, im) => n + (this._imageInUse(im) ? 1 : 0), 0);
|
||||
const counts = { all: images.length, inuse: inUse, unused: images.length - inUse };
|
||||
const chips = [
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'inuse', label: 'In use' },
|
||||
{ key: 'unused', label: 'Unused' },
|
||||
].map(c => {
|
||||
const on = this._imageFilter === c.key;
|
||||
return `<button type="button" class="sys-img-filter-btn${on ? ' is-active' : ''}" data-image-filter="${c.key}" aria-pressed="${on}">${c.label}<span class="sys-img-filter-n">${counts[c.key]}</span></button>`;
|
||||
}).join('');
|
||||
|
||||
const visible = this._sortImages(images).filter(im => this._imageMatchesFilter(im));
|
||||
|
||||
// Clear All / Select all act on the visible rows, so they only make
|
||||
// sense when the active filter actually has some.
|
||||
const actions = visible.length ? `
|
||||
<div class="sys-images-actions">
|
||||
<button type="button" class="clear-btn" data-images-clear id="sys-images-clear" title="Remove the images shown below">
|
||||
${trash}<span class="clear-btn-label">Clear All</span>
|
||||
</button>
|
||||
<label class="task-select-all" title="Select all images shown">
|
||||
<input type="checkbox" data-images-select-all id="sys-images-select-all">
|
||||
<span class="task-select-box" aria-hidden="true"></span>
|
||||
<span class="task-select-all-label">Select all</span>
|
||||
</label>
|
||||
</div>` : '';
|
||||
|
||||
const head = `
|
||||
<div class="sys-section-head sys-images-head">
|
||||
<h2>Images</h2>
|
||||
<div class="sys-images-toolbar">
|
||||
<button type="button" class="clear-btn" data-images-clear id="sys-images-clear" title="Remove all images">
|
||||
${trash}<span class="clear-btn-label">Clear All</span>
|
||||
</button>
|
||||
<label class="task-select-all" title="Select all images">
|
||||
<input type="checkbox" data-images-select-all id="sys-images-select-all">
|
||||
<span class="task-select-box" aria-hidden="true"></span>
|
||||
<span class="task-select-all-label">Select all</span>
|
||||
</label>
|
||||
<div class="sys-img-filter" role="group" aria-label="Filter images">${chips}</div>
|
||||
${actions}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const rows = images.map(im => {
|
||||
if (!visible.length) {
|
||||
const what = this._imageFilter === 'inuse' ? 'in-use' : 'unused';
|
||||
return `${head}<div class="sys-storage-app-empty">No ${what} images.</div>`;
|
||||
}
|
||||
|
||||
const rows = visible.map(im => {
|
||||
const id = fmt.escape(im.id || '');
|
||||
const pill = this._imagePill(im);
|
||||
const selected = this._selectedImageIds.has(im.id);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user