Sources: container labels in one field, colored by compose project
- One label per container (project/service) in a single field, like tag pickers: followed ones in the project color, a separator, then the others in grey; a click switches a label. Excluded ones last, locked. - Stopped containers hidden by default, shown dashed and still editable with 'Show stopped containers'; filter box; enable/disable all apply to the labels shown. - Docker app names in the log list use their compose project color. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd0307f6f6
commit
d751fb54ae
4 files changed
+146
-64
No files matched your search
@@ -69,13 +69,17 @@ colored and exported like syslog messages:
|
|||||||
each log also carries `container`, `container_id`, `image`, `compose_project`,
|
each log also carries `container`, `container_id`, `image`, `compose_project`,
|
||||||
`compose_service` and `stream` (stdout/stderr), visible in the row details.
|
`compose_service` and `stream` (stdout/stderr), visible in the row details.
|
||||||
- The **Source** filter shows only syslog or only Docker logs; Docker rows have a small cube
|
- The **Source** filter shows only syslog or only Docker logs; Docker rows have a small cube
|
||||||
before the app name.
|
before the app name, colored like its compose project.
|
||||||
- The severity comes from the line itself when the application writes it: JSON
|
- The severity comes from the line itself when the application writes it: JSON
|
||||||
(`"level":"error"`), logfmt (`level=warn`), `[ERROR]`, or an upper-case level word at the
|
(`"level":"error"`), logfmt (`level=warn`), `[ERROR]`, or an upper-case level word at the
|
||||||
start of the line (`ERROR`, `WARN`…). Otherwise it is `info`. Terminal color codes are removed.
|
start of the line (`ERROR`, `WARN`…). Otherwise it is `info`. Terminal color codes are removed.
|
||||||
- **Settings > Sources** lists the containers grouped by compose project, with a switch for
|
- **Settings > Sources** shows one label per container (`project/service`) in a single field:
|
||||||
each one, "Enable all" / "Disable all", and whether new containers are followed
|
followed containers in color, then not followed ones in grey; a click switches a label.
|
||||||
automatically (on by default). Choices are saved per compose service (or container name)
|
The color identifies the compose project, and Docker app names in the log list use the
|
||||||
|
same color. Stopped containers are hidden by default ("Show stopped containers" displays
|
||||||
|
them, dashed, and they stay editable). A filter box and "Enable all" / "Disable all" (applied
|
||||||
|
to the labels shown) help with many containers. New containers are followed automatically
|
||||||
|
unless that option is turned off. Choices are saved per compose service (or container name)
|
||||||
in `/data/docker.json`, so they survive re-creations.
|
in `/data/docker.json`, so they survive re-creations.
|
||||||
- Logstream remembers the position read in each container (`/data/docker-state.json`): after a
|
- Logstream remembers the position read in each container (`/data/docker-state.json`): after a
|
||||||
restart it resumes without losing or duplicating lines. A container seen for the first time
|
restart it resumes without losing or duplicating lines. A container seen for the first time
|
||||||
|
|||||||
+83
-36
@@ -73,8 +73,11 @@ const I18N = {
|
|||||||
dockerOff: 'The Docker source is disabled on this server. Set DOCKER_LOGS=on (see docker-compose.yml).',
|
dockerOff: 'The Docker source is disabled on this server. Set DOCKER_LOGS=on (see docker-compose.yml).',
|
||||||
dockerErr: 'Cannot reach Docker: ',
|
dockerErr: 'Cannot reach Docker: ',
|
||||||
dockerOk: ({ h, n, f }) => `Connected to Docker on ${h}: ${n} containers, ${f} followed.`,
|
dockerOk: ({ h, n, f }) => `Connected to Docker on ${h}: ${n} containers, ${f} followed.`,
|
||||||
dockerStandalone: 'Standalone containers',
|
dockerStopped: (n) => `Show stopped containers (${n})`,
|
||||||
stFollowing: 'followed', stIgnored: 'running, ignored', stStopped: 'stopped',
|
dockerFilter: 'Filter containers',
|
||||||
|
dockerLegend: 'Colored: followed · grey: not followed · dashed: stopped · the color identifies the compose project. Click a label to switch it.',
|
||||||
|
dockerNoMatch: 'No container',
|
||||||
|
stFollowing: 'followed', stSelected: 'followed when running', stIgnored: 'not followed', stStopped: 'stopped',
|
||||||
stLocked: 'excluded', stLockedTitle: 'Logstream itself, or label logstream.exclude=true',
|
stLocked: 'excluded', stLockedTitle: 'Logstream itself, or label logstream.exclude=true',
|
||||||
dockerHelp: 'Logs are read through docker-socket-proxy, a read-only gateway: Logstream can list containers and read their logs, nothing else. Choices apply per compose service (or container name), so they survive container re-creations.',
|
dockerHelp: 'Logs are read through docker-socket-proxy, a read-only gateway: Logstream can list containers and read their logs, nothing else. Choices apply per compose service (or container name), so they survive container re-creations.',
|
||||||
fContainer: 'container', fContainerId: 'container ID', fImage: 'image', fProject: 'compose project',
|
fContainer: 'container', fContainerId: 'container ID', fImage: 'image', fProject: 'compose project',
|
||||||
@@ -174,8 +177,11 @@ const I18N = {
|
|||||||
dockerOff: 'La source Docker est désactivée sur ce serveur. Réglez DOCKER_LOGS=on (voir docker-compose.yml).',
|
dockerOff: 'La source Docker est désactivée sur ce serveur. Réglez DOCKER_LOGS=on (voir docker-compose.yml).',
|
||||||
dockerErr: 'Docker injoignable : ',
|
dockerErr: 'Docker injoignable : ',
|
||||||
dockerOk: ({ h, n, f }) => `Connecté à Docker sur ${h} : ${n} conteneurs, ${f} suivis.`,
|
dockerOk: ({ h, n, f }) => `Connecté à Docker sur ${h} : ${n} conteneurs, ${f} suivis.`,
|
||||||
dockerStandalone: 'Conteneurs isolés',
|
dockerStopped: (n) => `Afficher les conteneurs arrêtés (${n})`,
|
||||||
stFollowing: 'suivi', stIgnored: 'actif, ignoré', stStopped: 'arrêté',
|
dockerFilter: 'Filtrer les conteneurs',
|
||||||
|
dockerLegend: 'En couleur : suivis · en gris : non suivis · pointillés : arrêtés · la couleur identifie le projet compose. Cliquez sur une étiquette pour la basculer.',
|
||||||
|
dockerNoMatch: 'Aucun conteneur',
|
||||||
|
stFollowing: 'suivi', stSelected: 'suivi dès qu\'il tourne', stIgnored: 'non suivi', stStopped: 'arrêté',
|
||||||
stLocked: 'exclu', stLockedTitle: 'Logstream lui-même, ou étiquette logstream.exclude=true',
|
stLocked: 'exclu', stLockedTitle: 'Logstream lui-même, ou étiquette logstream.exclude=true',
|
||||||
dockerHelp: 'Les logs sont lus via docker-socket-proxy, une passerelle en lecture seule : Logstream peut lister les conteneurs et lire leurs logs, rien d\'autre. Les choix s\'appliquent par service compose (ou nom de conteneur), ils survivent donc à la recréation des conteneurs.',
|
dockerHelp: 'Les logs sont lus via docker-socket-proxy, une passerelle en lecture seule : Logstream peut lister les conteneurs et lire leurs logs, rien d\'autre. Les choix s\'appliquent par service compose (ou nom de conteneur), ils survivent donc à la recréation des conteneurs.',
|
||||||
fContainer: 'conteneur', fContainerId: 'ID du conteneur', fImage: 'image', fProject: 'projet compose',
|
fContainer: 'conteneur', fContainerId: 'ID du conteneur', fImage: 'image', fProject: 'projet compose',
|
||||||
@@ -370,6 +376,7 @@ function applyLang() {
|
|||||||
for (const el of document.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
|
for (const el of document.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
|
||||||
for (const el of document.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
|
for (const el of document.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
|
||||||
for (const el of document.querySelectorAll('[data-i18n-aria]')) el.setAttribute('aria-label', t(el.dataset.i18nAria));
|
for (const el of document.querySelectorAll('[data-i18n-aria]')) el.setAttribute('aria-label', t(el.dataset.i18nAria));
|
||||||
|
for (const el of document.querySelectorAll('[data-i18n-ph]')) el.placeholder = t(el.dataset.i18nPh);
|
||||||
for (const b of document.querySelectorAll('#langSwitch [data-lang]')) b.setAttribute('aria-checked', String(b.dataset.lang === lang));
|
for (const b of document.querySelectorAll('#langSwitch [data-lang]')) b.setAttribute('aria-checked', String(b.dataset.lang === lang));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,12 +652,20 @@ function rowHTML(r, isNew) {
|
|||||||
? `<span class="host" data-act="host" title="${esc(hostTitle(r) + ' · ' + t('clickHost'))}">${esc(r.host_name || r.host)}</span>`
|
? `<span class="host" data-act="host" title="${esc(hostTitle(r) + ' · ' + t('clickHost'))}">${esc(r.host_name || r.host)}</span>`
|
||||||
: '<span class="host"></span>')
|
: '<span class="host"></span>')
|
||||||
+ (r.app
|
+ (r.app
|
||||||
? `<span class="app" data-act="app" title="${esc((r.container ? r.container + ' · ' : '') + r.app + ' · ' + t('clickApp'))}">${r.source_type === 'docker' ? DOCKER_ICON : ''}${esc(r.app)}</span>`
|
? `<span class="app${r.source_type === 'docker' ? ' proj' : ''}" data-act="app"${r.source_type === 'docker' ? ` style="--h:${hueOf(r.compose_project || r.container || r.app)}"` : ''} title="${esc((r.compose_project ? r.compose_project + '/' : '') + (r.container ? r.container + ' · ' : '') + r.app + ' · ' + t('clickApp'))}">${r.source_type === 'docker' ? DOCKER_ICON : ''}${esc(r.app)}</span>`
|
||||||
: '<span class="app"></span>')
|
: '<span class="app"></span>')
|
||||||
+ `<div class="msg">${highlight(r._msg)}</div>`
|
+ `<div class="msg">${highlight(r._msg)}</div>`
|
||||||
+ '</article>';
|
+ '</article>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable hue (0-359) for a name: the compose project color, shared by the
|
||||||
|
// Sources labels and the Docker app names in the log list.
|
||||||
|
function hueOf(name) {
|
||||||
|
let h = 0;
|
||||||
|
for (const ch of String(name)) h = (h * 31 + ch.codePointAt(0)) >>> 0;
|
||||||
|
return h % 360;
|
||||||
|
}
|
||||||
|
|
||||||
const DOCKER_ICON = '<svg class="src-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg>';
|
const DOCKER_ICON = '<svg class="src-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m3 8 9 5 9-5M12 13v8"/></svg>';
|
||||||
|
|
||||||
// Tooltip of the host cell: name and IP when both are known.
|
// Tooltip of the host cell: name and IP when both are known.
|
||||||
@@ -1083,12 +1098,39 @@ document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') setExpor
|
|||||||
/* ================= Docker source ================= */
|
/* ================= Docker source ================= */
|
||||||
|
|
||||||
let dockerState = null;
|
let dockerState = null;
|
||||||
|
const dockerView = { showStopped: store.get('dockerShowStopped', '0') === '1', filter: '' };
|
||||||
|
const LOCK_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
|
||||||
|
|
||||||
async function loadDocker() {
|
async function loadDocker() {
|
||||||
try { dockerState = await api('/api/docker'); } catch (e) { dockerState = { enabled: true, connected: false, error: e.message }; }
|
try { dockerState = await api('/api/docker'); } catch (e) { dockerState = { enabled: true, connected: false, error: e.message }; }
|
||||||
renderDocker();
|
renderDocker();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One label per choice key ("project/service"): replicas of a service share it.
|
||||||
|
function dockerChips() {
|
||||||
|
const byKey = new Map();
|
||||||
|
for (const c of (dockerState && dockerState.containers) || []) {
|
||||||
|
const k = byKey.get(c.key);
|
||||||
|
if (!k) {
|
||||||
|
byKey.set(c.key, { ...c, names: [c.name], running: c.state === 'running', following: c.following });
|
||||||
|
} else {
|
||||||
|
k.names.push(c.name);
|
||||||
|
k.running = k.running || c.state === 'running';
|
||||||
|
k.following = k.following || c.following;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
function chipHTML(c) {
|
||||||
|
const state = c.locked ? t('stLocked') : !c.running ? t('stStopped') : c.following ? t('stFollowing') : c.enabled ? t('stSelected') : t('stIgnored');
|
||||||
|
const title = [c.key, c.names.join(', '), c.image, state + (c.locked ? ` (${t('stLockedTitle')})` : '')].join('\n');
|
||||||
|
const cls = ['chip', c.enabled && !c.locked ? 'on' : '', c.running ? '' : 'stopped', c.locked ? 'locked' : ''].filter(Boolean).join(' ');
|
||||||
|
return `<button type="button" class="${cls}" style="--h:${hueOf(c.project || c.name)}" data-key="${esc(c.key)}"`
|
||||||
|
+ ` aria-pressed="${c.enabled && !c.locked}"${c.locked ? ' disabled' : ''} title="${esc(title)}">`
|
||||||
|
+ `${c.locked ? LOCK_ICON : ''}${esc(c.key)}</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderDocker() {
|
function renderDocker() {
|
||||||
const status = $('#dockerStatus');
|
const status = $('#dockerStatus');
|
||||||
const body = $('#dockerBody');
|
const body = $('#dockerBody');
|
||||||
@@ -1110,33 +1152,20 @@ function renderDocker() {
|
|||||||
}
|
}
|
||||||
body.hidden = !cs.length && !d.connected;
|
body.hidden = !cs.length && !d.connected;
|
||||||
$('#dockerDefault').checked = !!d.defaultEnabled;
|
$('#dockerDefault').checked = !!d.defaultEnabled;
|
||||||
|
$('#dockerStopped').checked = dockerView.showStopped;
|
||||||
|
|
||||||
// Group by compose project; standalone containers last.
|
const chips = dockerChips();
|
||||||
const groups = new Map();
|
$('#dockerStoppedLbl').textContent = t('dockerStopped', chips.filter((c) => !c.running && !c.locked).length);
|
||||||
for (const c of cs) {
|
const q = dockerView.filter.toLowerCase();
|
||||||
const g = c.project || '';
|
const visible = chips.filter((c) => (dockerView.showStopped || c.running) && (!q || c.key.toLowerCase().includes(q) || c.image.toLowerCase().includes(q)));
|
||||||
if (!groups.has(g)) groups.set(g, []);
|
const groups = [
|
||||||
groups.get(g).push(c);
|
visible.filter((c) => c.enabled && !c.locked),
|
||||||
}
|
visible.filter((c) => !c.enabled && !c.locked),
|
||||||
const names = [...groups.keys()].sort((a, b) => (a === '') - (b === '') || a.localeCompare(b));
|
visible.filter((c) => c.locked),
|
||||||
let html = '';
|
].filter((g) => g.length);
|
||||||
for (const g of names) {
|
$('#dockerList').innerHTML = groups.length
|
||||||
html += `<div class="dk-group"><div class="dk-group-title">${esc(g || t('dockerStandalone'))}</div>`;
|
? groups.map((g) => g.map(chipHTML).join('')).join('<span class="chip-sep" aria-hidden="true"></span>')
|
||||||
for (const c of groups.get(g)) {
|
: `<span class="chip-empty">${esc(t('dockerNoMatch'))}</span>`;
|
||||||
let st = 'stopped';
|
|
||||||
let label = t('stStopped');
|
|
||||||
if (c.locked) { st = 'locked'; label = t('stLocked'); }
|
|
||||||
else if (c.following) { st = 'following'; label = t('stFollowing'); }
|
|
||||||
else if (c.state === 'running') { st = 'ignored'; label = t('stIgnored'); }
|
|
||||||
html += `<label class="dk-row${c.locked ? ' locked' : ''}">
|
|
||||||
<input type="checkbox" class="switch" data-key="${esc(c.key)}"${c.enabled ? ' checked' : ''}${c.locked ? ' disabled' : ''}>
|
|
||||||
<span class="dk-main"><strong>${esc(c.service || c.name)}</strong><small>${esc(c.name)} · ${esc(c.image)}</small></span>
|
|
||||||
<span class="dk-state ${st}"${c.locked ? ` title="${esc(t('stLockedTitle'))}"` : ''}>${esc(label)}</span>
|
|
||||||
</label>`;
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
$('#dockerList').innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureDocker(body) {
|
async function configureDocker(body) {
|
||||||
@@ -1144,18 +1173,36 @@ async function configureDocker(body) {
|
|||||||
dockerState = await api('/api/docker', { method: 'PUT', body });
|
dockerState = await api('/api/docker', { method: 'PUT', body });
|
||||||
renderDocker();
|
renderDocker();
|
||||||
setTimeout(loadDocker, 1500); // followers start or stop in the background
|
setTimeout(loadDocker, 1500); // followers start or stop in the background
|
||||||
} catch (e) { toast(e.message); }
|
} catch (e) { toast(e.message); loadDocker(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#dockerDefault').addEventListener('change', (ev) => configureDocker({ defaultEnabled: ev.target.checked }));
|
$('#dockerDefault').addEventListener('change', (ev) => configureDocker({ defaultEnabled: ev.target.checked }));
|
||||||
$('#dockerList').addEventListener('change', (ev) => {
|
$('#dockerStopped').addEventListener('change', (ev) => {
|
||||||
const key = ev.target.dataset.key;
|
dockerView.showStopped = ev.target.checked;
|
||||||
if (key) configureDocker({ containers: { [key]: ev.target.checked } });
|
store.set('dockerShowStopped', dockerView.showStopped ? '1' : '0');
|
||||||
|
renderDocker();
|
||||||
|
});
|
||||||
|
$('#dockerFilter').addEventListener('input', (ev) => { dockerView.filter = ev.target.value.trim(); renderDocker(); });
|
||||||
|
$('#dockerList').addEventListener('click', (ev) => {
|
||||||
|
const chip = ev.target.closest('.chip[data-key]');
|
||||||
|
if (!chip || chip.disabled) return;
|
||||||
|
const key = chip.dataset.key;
|
||||||
|
const on = !chip.classList.contains('on');
|
||||||
|
// Immediate feedback, confirmed by the server answer.
|
||||||
|
for (const c of (dockerState && dockerState.containers) || []) if (c.key === key) c.enabled = on;
|
||||||
|
renderDocker();
|
||||||
|
configureDocker({ containers: { [key]: on } });
|
||||||
});
|
});
|
||||||
for (const [id, on] of [['#dockerAll', true], ['#dockerNone', false]]) {
|
for (const [id, on] of [['#dockerAll', true], ['#dockerNone', false]]) {
|
||||||
$(id).addEventListener('click', () => {
|
$(id).addEventListener('click', () => {
|
||||||
|
// Applies to the labels currently shown (filter and stopped option included).
|
||||||
|
const q = dockerView.filter.toLowerCase();
|
||||||
const containers = {};
|
const containers = {};
|
||||||
for (const c of (dockerState && dockerState.containers) || []) if (!c.locked) containers[c.key] = on;
|
for (const c of dockerChips()) {
|
||||||
|
if (c.locked || (!dockerView.showStopped && !c.running)) continue;
|
||||||
|
if (q && !c.key.toLowerCase().includes(q) && !c.image.toLowerCase().includes(q)) continue;
|
||||||
|
containers[c.key] = on;
|
||||||
|
}
|
||||||
configureDocker({ containers });
|
configureDocker({ containers });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -182,15 +182,23 @@
|
|||||||
<h3 data-i18n="dockerTitle">Docker containers</h3>
|
<h3 data-i18n="dockerTitle">Docker containers</h3>
|
||||||
<p id="dockerStatus" class="docker-status"></p>
|
<p id="dockerStatus" class="docker-status"></p>
|
||||||
<div id="dockerBody" hidden>
|
<div id="dockerBody" hidden>
|
||||||
|
<div class="dk-opts">
|
||||||
<label class="switch-row">
|
<label class="switch-row">
|
||||||
<input id="dockerDefault" type="checkbox" class="switch">
|
<input id="dockerDefault" type="checkbox" class="switch">
|
||||||
<span data-i18n="dockerDefault">Follow new containers automatically</span>
|
<span data-i18n="dockerDefault">Follow new containers automatically</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="docker-actions">
|
<label class="switch-row">
|
||||||
<button id="dockerAll" class="btn" type="button" data-i18n="dockerAll">Enable all</button>
|
<input id="dockerStopped" type="checkbox" class="switch">
|
||||||
<button id="dockerNone" class="btn ghost" type="button" data-i18n="dockerNone">Disable all</button>
|
<span id="dockerStoppedLbl">Show stopped containers</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="dockerList" class="docker-list"></div>
|
<div class="dk-toolbar">
|
||||||
|
<input id="dockerFilter" type="search" class="dk-filter" autocomplete="off" spellcheck="false" data-i18n-ph="dockerFilter" data-i18n-aria="dockerFilter">
|
||||||
|
<button id="dockerAll" class="link-btn" type="button" data-i18n="dockerAll">Enable all</button>
|
||||||
|
<button id="dockerNone" class="link-btn" type="button" data-i18n="dockerNone">Disable all</button>
|
||||||
|
</div>
|
||||||
|
<div id="dockerList" class="chip-field"></div>
|
||||||
|
<p class="dk-legend" data-i18n="dockerLegend"></p>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted small" data-i18n="dockerHelp"></p>
|
<p class="muted small" data-i18n="dockerHelp"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+43
-20
@@ -11,6 +11,7 @@
|
|||||||
--row-hover: #f5f6fa;
|
--row-hover: #f5f6fa;
|
||||||
--hit: rgba(99, 102, 241, .18);
|
--hit: rgba(99, 102, 241, .18);
|
||||||
--shadow: 0 10px 30px rgba(15, 23, 42, .12);
|
--shadow: 0 10px 30px rgba(15, 23, 42, .12);
|
||||||
|
--chip-l: 38%; /* lightness of project-colored text (labels, Docker apps) */
|
||||||
|
|
||||||
--sev-crit: #be123c;
|
--sev-crit: #be123c;
|
||||||
--sev-err: #dc2626;
|
--sev-err: #dc2626;
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
--row-hover: #1a1f26;
|
--row-hover: #1a1f26;
|
||||||
--hit: rgba(129, 140, 248, .28);
|
--hit: rgba(129, 140, 248, .28);
|
||||||
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
|
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
|
||||||
|
--chip-l: 70%;
|
||||||
--sev-crit: #fb7185;
|
--sev-crit: #fb7185;
|
||||||
--sev-err: #f87171;
|
--sev-err: #f87171;
|
||||||
--sev-warning: #fbbf24;
|
--sev-warning: #fbbf24;
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
--row-hover: #1a1f26;
|
--row-hover: #1a1f26;
|
||||||
--hit: rgba(129, 140, 248, .28);
|
--hit: rgba(129, 140, 248, .28);
|
||||||
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
|
--shadow: 0 10px 30px rgba(0, 0, 0, .5);
|
||||||
|
--chip-l: 70%;
|
||||||
--sev-crit: #fb7185;
|
--sev-crit: #fb7185;
|
||||||
--sev-err: #f87171;
|
--sev-err: #f87171;
|
||||||
--sev-warning: #fbbf24;
|
--sev-warning: #fbbf24;
|
||||||
@@ -220,6 +223,9 @@ body.busy .progress::after {
|
|||||||
|
|
||||||
/* Docker logs */
|
/* Docker logs */
|
||||||
.src-ico { width: 1em; height: 1em; vertical-align: -.15em; margin-right: 4px; color: #2496ed; }
|
.src-ico { width: 1em; height: 1em; vertical-align: -.15em; margin-right: 4px; color: #2496ed; }
|
||||||
|
/* Docker app names take the color of their compose project (see hueOf in app.js) */
|
||||||
|
.row .app.proj { color: hsl(var(--h) 55% var(--chip-l)); }
|
||||||
|
.row .app.proj .src-ico { color: inherit; }
|
||||||
|
|
||||||
/* ---------- Histogram ---------- */
|
/* ---------- Histogram ---------- */
|
||||||
.histo { padding: 0 20px 6px; }
|
.histo { padding: 0 20px 6px; }
|
||||||
@@ -418,28 +424,45 @@ select.field:focus { outline: 0; border-color: var(--accent); box-shadow: 0 0 0
|
|||||||
.docker-status.good::before, .docker-status.bad::before { content: "●"; margin-right: 6px; }
|
.docker-status.good::before, .docker-status.bad::before { content: "●"; margin-right: 6px; }
|
||||||
.docker-status.good::before { color: var(--sev-info); }
|
.docker-status.good::before { color: var(--sev-info); }
|
||||||
.docker-status.bad { color: var(--sev-err); }
|
.docker-status.bad { color: var(--sev-err); }
|
||||||
.switch-row { display: flex; align-items: center; gap: 10px; font-size: 13.5px; font-weight: 550; cursor: pointer; }
|
.switch-row { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 550; cursor: pointer; }
|
||||||
.docker-actions { display: flex; gap: 8px; margin: 12px 0; }
|
.dk-opts { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.docker-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 12px; }
|
.dk-toolbar { display: flex; align-items: center; gap: 12px; margin: 14px 0 6px; }
|
||||||
.dk-group { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
.dk-filter {
|
||||||
.dk-group-title {
|
flex: 1; min-width: 0; height: 28px; padding: 0 10px;
|
||||||
padding: 7px 12px; background: var(--panel-2);
|
border: 1px solid var(--border); border-radius: 7px; background: var(--panel);
|
||||||
font-size: 12px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
|
font: 12px var(--mono); outline: 0;
|
||||||
}
|
}
|
||||||
.dk-row {
|
.dk-filter:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
||||||
display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px;
|
.link-btn { border: 0; background: none; padding: 0; font-size: 12px; font-weight: 550; color: var(--accent); white-space: nowrap; }
|
||||||
padding: 8px 12px; border-top: 1px solid var(--border); cursor: pointer;
|
.link-btn:hover { text-decoration: underline; }
|
||||||
|
.dk-legend { font-size: 11.5px; color: var(--muted); margin: 8px 0 12px !important; }
|
||||||
|
|
||||||
|
/* Container labels (Settings > Sources) */
|
||||||
|
.chip-field {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
|
||||||
|
padding: 8px; min-height: 40px;
|
||||||
|
border: 1px solid var(--border); border-radius: 10px; background: var(--panel);
|
||||||
}
|
}
|
||||||
.dk-row:hover { background: var(--row-hover); }
|
.chip {
|
||||||
.dk-row.locked { cursor: default; opacity: .65; }
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
.dk-main { display: flex; flex-direction: column; min-width: 0; }
|
height: 22px; padding: 0 7px;
|
||||||
.dk-main strong { font-size: 13.5px; font-weight: 600; }
|
border: 1px solid var(--border); border-radius: 5px; background: transparent;
|
||||||
.dk-main small { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
color: var(--muted); font: 11px/1 var(--mono); white-space: nowrap; cursor: pointer;
|
||||||
.dk-state { font-size: 12px; white-space: nowrap; color: var(--muted); }
|
}
|
||||||
.dk-state::before { content: "●"; margin-right: 5px; }
|
.chip:hover { color: var(--text); border-color: var(--muted); }
|
||||||
.dk-state.following { color: var(--sev-info); }
|
.chip.on {
|
||||||
.dk-state.ignored::before { color: var(--sev-warning); }
|
color: hsl(var(--h) 55% var(--chip-l));
|
||||||
.dk-state.stopped::before, .dk-state.locked::before { opacity: .4; }
|
border-color: hsl(var(--h) 50% var(--chip-l) / .7);
|
||||||
|
background: hsl(var(--h) 60% 50% / .13);
|
||||||
|
}
|
||||||
|
.chip.on:hover { background: hsl(var(--h) 60% 50% / .22); }
|
||||||
|
.chip.stopped { border-style: dashed; }
|
||||||
|
.chip.stopped:not(.on) { opacity: .7; }
|
||||||
|
.chip.locked { opacity: .45; cursor: not-allowed; }
|
||||||
|
.chip svg { width: 10px; height: 10px; }
|
||||||
|
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.chip-sep { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }
|
||||||
|
.chip-empty { font-size: 12px; color: var(--muted); padding: 0 4px; }
|
||||||
|
|
||||||
/* Toggle switch */
|
/* Toggle switch */
|
||||||
input.switch {
|
input.switch {
|
||||||
|
|||||||
Reference in new issue
Block a user