blob: d73546cf847be0a910c09b66ff7bd7b5b4ec990c [file] [log] [blame]
/**
* @module modules/machine-server
* @description <h2><code>machine-server</code></h2>
*
* The main machine server landing page.
*
* Uses local storage to persist the user's choice of auto-refresh.
*
* @attr waiting - If present then display the waiting cursor.
*/
import { html } from 'lit-html';
import { errorMessage } from 'elements-sk/errorMessage';
import { diffDate } from 'common-sk/modules/human';
import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import '../../../infra-sk/modules/theme-chooser-sk';
import 'elements-sk/error-toast-sk';
import 'elements-sk/icon/cached-icon-sk';
import 'elements-sk/icon/pause-icon-sk';
import 'elements-sk/icon/play-arrow-icon-sk';
import 'elements-sk/styles/buttons';
const REFRESH_LOCALSTORAGE_KEY = 'autorefresh';
const temps = (temperatures) => {
if (!temperatures) {
return '';
}
const values = Object.values(temperatures);
if (!values.length) {
return '';
}
let total = 0;
values.forEach((x) => { total += x; });
const ave = total / values.length;
return html`
<details>
<summary>Avg: ${ave.toFixed(1)}</summary>
<table>
${Object.entries(temperatures).map((pair) => html`<tr><td>${pair[0]}</td><td>${pair[1]}</td></tr>`)}
</table>
</details>
`;
};
const isRunning = (machine) => (machine.RunningSwarmingTask ? html`<cached-icon-sk title="Running"></cached-icon-sk>` : '');
const asList = (arr) => arr.join(' | ');
const dimensions = (machine) => {
if (!machine.Dimensions) {
return '';
}
return html`
<details>
<summary>Dimensions</summary>
<table>
${Object.entries(machine.Dimensions).map((pair) => html`<tr><td>${pair[0]}</td><td>${asList(pair[1])}</td></tr>`)}
</table>
</details>
`;
};
const annotation = (machine) => {
if (!machine.Annotation.Message) {
return '';
}
return html`
${machine.Annotation.User} (${diffDate(machine.Annotation.Timestamp)}) - ${machine.Annotation.Message}
`;
};
const update = (machine) => {
if (machine.ScheduledForDeletion) {
return 'Waiting for update.';
}
return 'Update';
};
const imageName = (machine) => {
// KubernetesImage looks like:
// "gcr.io/skia-public/rpi-swarming-client:2020-05-09T19_28_20Z-jcgregorio-4fef3ca-clean".
// We just need to display everything after the ":".
const parts = machine.KubernetesImage.split(':');
if (parts.length < 2) {
return '(missing)';
}
return parts[1];
};
const rows = (ele) => ele._machines.map((machine) => html`
<tr id=${machine.Dimensions.id}>
<td><a href="https://chromium-swarm.appspot.com/bot?id=${machine.Dimensions.id}">${machine.Dimensions.id}</a></td>
<td>${machine.PodName}</td>
<td>${machine.Dimensions.device_type}</td>
<td><button class=mode @click=${() => ele._toggleMode(machine.Dimensions.id)}>${machine.Mode}</button></td>
<td><button class=update @click=${() => ele._toggleUpdate(machine.Dimensions.id)}>${update(machine)}</button></td>
<td>${machine.Dimensions.quarantined}</td>
<td>${isRunning(machine)}</td>
<td>${machine.Battery}</td>
<td>
${temps(machine.Temperature)}
</td>
<td>${diffDate(machine.LastUpdated)}</td>
<td>${dimensions(machine)}</td>
<td>${annotation(machine)}</td>
<td>${imageName(machine)}</td>
</tr>
`);
const refreshButtonDisplayValue = (ele) => {
if (ele.refreshing) {
return html`<pause-icon-sk></pause-icon-sk>`;
}
return html`<play-arrow-icon-sk></play-arrow-icon-sk>`;
};
const template = (ele) => html`
<header>
<span
id=refresh
@click=${() => ele._toggleRefresh()}
title="Start/Stop the automatic refreshing of data on the page."
>${refreshButtonDisplayValue(ele)}</span>
<theme-chooser-sk
title="Toggle between light and dark mode."
></theme-chooser-sk>
</header>
<main>
<table>
<tr>
<th>Machine</th>
<th>Pod</th>
<th>Device</th>
<th>Mode</th>
<th>Update</th>
<th>Quarantined</th>
<th>Task</th>
<th>Battery</th>
<th>Temperature</th>
<th>Last Seen</th>
<th>Dimensions</th>
<th>Annotation</th>
<th>Image</th>
</tr>
${rows(ele)}
</table>
</main>
<error-toast-sk></error-toast-sk>
`;
window.customElements.define('machine-server-sk', class extends ElementSk {
constructor() {
super(template);
this._machines = [];
// The id of the running setTimeout, if any, otherwise 0.
this._timeout = 0;
}
connectedCallback() {
super.connectedCallback();
this._render();
this._refreshStep();
}
/** @prop refreshing {bool} True if the data on the page is periodically refreshed. */
get refreshing() { return window.localStorage.getItem(REFRESH_LOCALSTORAGE_KEY) === 'true'; }
set refreshing(val) { window.localStorage.setItem(REFRESH_LOCALSTORAGE_KEY, !!val); }
_onError(msg) {
this.removeAttribute('waiting');
errorMessage(msg);
}
async _update(changeCursor = false) {
if (changeCursor) {
this.setAttribute('waiting', '');
}
try {
const resp = await fetch('/_/machines');
const json = await jsonOrThrow(resp);
if (changeCursor) {
this.removeAttribute('waiting');
}
this._machines = json;
this._render();
} catch (error) {
this._onError(error);
}
}
async _toggleMode(id) {
try {
this.setAttribute('waiting', '');
await fetch(`/_/machine/toggle_mode/${id}`);
this.removeAttribute('waiting');
this._update(true);
} catch (error) {
this._onError(error);
}
}
async _toggleUpdate(id) {
try {
this.setAttribute('waiting', '');
await fetch(`/_/machine/toggle_update/${id}`);
this.removeAttribute('waiting');
this._update(true);
} catch (error) {
this._onError(error);
}
}
async _refreshStep() {
// Wait for _update to finish so we don't pile up requests if server latency
// rises.
await this._update();
if (this.refreshing && this._timeout === 0) {
this._timeout = window.setTimeout(() => {
// Only done here, so multiple calls to _refreshStep() won't start
// parallel setTimeout chains.
this._timeout = 0;
this._refreshStep();
}, 2000);
}
}
_toggleRefresh() {
this.refreshing = !this.refreshing;
this._refreshStep();
}
disconnectedCallback() {
}
});