mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Add Hardware Page to Configuration System Menu (#12405)
This commit is contained in:
parent
dee59486ba
commit
8e55c83996
@ -397,6 +397,10 @@ class HaPanelConfig extends HassRouterPage {
|
|||||||
tag: "ha-config-energy",
|
tag: "ha-config-energy",
|
||||||
load: () => import("./energy/ha-config-energy"),
|
load: () => import("./energy/ha-config-energy"),
|
||||||
},
|
},
|
||||||
|
hardware: {
|
||||||
|
tag: "ha-config-hardware",
|
||||||
|
load: () => import("./hardware/ha-config-hardware"),
|
||||||
|
},
|
||||||
integrations: {
|
integrations: {
|
||||||
tag: "ha-config-integrations",
|
tag: "ha-config-integrations",
|
||||||
load: () => import("./integrations/ha-config-integrations"),
|
load: () => import("./integrations/ha-config-integrations"),
|
||||||
|
213
src/panels/config/hardware/dialog-hardware-available.ts
Normal file
213
src/panels/config/hardware/dialog-hardware-available.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
|
import "../../../components/ha-dialog";
|
||||||
|
import "../../../components/ha-expansion-panel";
|
||||||
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/search-input";
|
||||||
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
|
import {
|
||||||
|
fetchHassioHardwareInfo,
|
||||||
|
HassioHardwareInfo,
|
||||||
|
} from "../../../data/hassio/hardware";
|
||||||
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
|
import { dump } from "../../../resources/js-yaml-dump";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
const _filterDevices = memoizeOne(
|
||||||
|
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
|
||||||
|
hardware.devices
|
||||||
|
.filter(
|
||||||
|
(device) =>
|
||||||
|
(showAdvanced ||
|
||||||
|
["tty", "gpio", "input"].includes(device.subsystem)) &&
|
||||||
|
(device.by_id?.toLowerCase().includes(filter) ||
|
||||||
|
device.name.toLowerCase().includes(filter) ||
|
||||||
|
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||||
|
JSON.stringify(device.attributes)
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(filter))
|
||||||
|
)
|
||||||
|
.sort((a, b) => stringCompare(a.name, b.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
@customElement("ha-dialog-hardware-available")
|
||||||
|
class DialogHardwareAvailable extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _hardware?: HassioHardwareInfo;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
public async showDialog(): Promise<Promise<void>> {
|
||||||
|
try {
|
||||||
|
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||||
|
} catch (err: any) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
||||||
|
),
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._hardware = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._hardware) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = _filterDevices(
|
||||||
|
this.hass.userData?.showAdvanced || false,
|
||||||
|
this._hardware,
|
||||||
|
(this._filter || "").toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
hideActions
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="header" slot="heading">
|
||||||
|
<h2>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.title"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
dialogAction="close"
|
||||||
|
></ha-icon-button>
|
||||||
|
<search-input
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
.label=${this.hass.localize("common.search")}
|
||||||
|
>
|
||||||
|
</search-input>
|
||||||
|
</div>
|
||||||
|
${devices.map(
|
||||||
|
(device) =>
|
||||||
|
html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
.header=${device.name}
|
||||||
|
.secondary=${device.by_id || undefined}
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
<div class="device-property">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.subsystem"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<span>${device.subsystem}</span>
|
||||||
|
</div>
|
||||||
|
<div class="device-property">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.device_path"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<code>${device.dev_path}</code>
|
||||||
|
</div>
|
||||||
|
${device.by_id
|
||||||
|
? html`
|
||||||
|
<div class="device-property">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.id"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<code>${device.by_id}</code>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="attributes">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.attributes"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<pre>${dump(device.attributes, { indent: 2 })}</pre>
|
||||||
|
</div>
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-icon-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 18px 42px 0 18px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
background-color: var(--markdown-code-background-color, none);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-size: 85%;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
}
|
||||||
|
search-input {
|
||||||
|
margin: 8px 16px 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.device-property {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.attributes {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-hardware-available": DialogHardwareAvailable;
|
||||||
|
}
|
||||||
|
}
|
257
src/panels/config/hardware/ha-config-hardware.ts
Normal file
257
src/panels/config/hardware/ha-config-hardware.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiDotsVertical } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import "../../../components/buttons/ha-progress-button";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import {
|
||||||
|
extractApiErrorMessage,
|
||||||
|
ignoreSupervisorError,
|
||||||
|
} from "../../../data/hassio/common";
|
||||||
|
import {
|
||||||
|
fetchHassioHassOsInfo,
|
||||||
|
fetchHassioHostInfo,
|
||||||
|
HassioHassOSInfo,
|
||||||
|
HassioHostInfo,
|
||||||
|
rebootHost,
|
||||||
|
shutdownHost,
|
||||||
|
} from "../../../data/hassio/host";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
||||||
|
|
||||||
|
@customElement("ha-config-hardware")
|
||||||
|
class HaConfigHardware extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@state() private _error?: { code: string; message: string };
|
||||||
|
|
||||||
|
@state() private _OSData?: HassioHassOSInfo;
|
||||||
|
|
||||||
|
@state() private _hostData?: HassioHostInfo;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
if (isComponentLoaded(this.hass, "hassio")) {
|
||||||
|
this._load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
back-path="/config/system"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
|
||||||
|
>
|
||||||
|
${this._error
|
||||||
|
? html`
|
||||||
|
<ha-alert alert-type="error"
|
||||||
|
>${this._error.message || this._error.code}</ha-alert
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._OSData && this._hostData
|
||||||
|
? html`
|
||||||
|
<div class="content">
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.board"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<div slot="description">
|
||||||
|
<span class="value">${this._OSData.board}</span>
|
||||||
|
</div>
|
||||||
|
</ha-settings-row>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<div class="buttons">
|
||||||
|
${this._hostData.features.includes("reboot")
|
||||||
|
? html`
|
||||||
|
<ha-progress-button
|
||||||
|
class="warning"
|
||||||
|
@click=${this._hostReboot}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.reboot_host"
|
||||||
|
)}
|
||||||
|
</ha-progress-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${this._hostData.features.includes("shutdown")
|
||||||
|
? html`
|
||||||
|
<ha-progress-button
|
||||||
|
class="warning"
|
||||||
|
@click=${this._hostShutdown}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.shutdown_host"
|
||||||
|
)}
|
||||||
|
</ha-progress-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<ha-button-menu corner="BOTTOM_START">
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
slot="trigger"
|
||||||
|
></ha-icon-button>
|
||||||
|
<mwc-list-item
|
||||||
|
.action=${"hardware"}
|
||||||
|
@click=${this._openHardware}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.available_hardware.title"
|
||||||
|
)}
|
||||||
|
</mwc-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _load() {
|
||||||
|
try {
|
||||||
|
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||||
|
this._hostData = await fetchHassioHostInfo(this.hass);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err.message || err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openHardware() {
|
||||||
|
showhardwareAvailableDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||||
|
text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"),
|
||||||
|
confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"),
|
||||||
|
dismissText: this.hass.localize("common.cancel"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rebootHost(this.hass);
|
||||||
|
} catch (err: any) {
|
||||||
|
// Ignore connection errors, these are all expected
|
||||||
|
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.failed_to_reboot_host"
|
||||||
|
),
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as any;
|
||||||
|
button.progress = true;
|
||||||
|
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.shutdown_host_confirm"
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"),
|
||||||
|
dismissText: this.hass.localize("common.cancel"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
button.progress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await shutdownHost(this.hass);
|
||||||
|
} catch (err: any) {
|
||||||
|
// Ignore connection errors, these are all expected
|
||||||
|
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.failed_to_shutdown_host"
|
||||||
|
),
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
--mdc-menu-min-width: 200px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
height: 48px;
|
||||||
|
border-top: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-hardware": HaConfigHardware;
|
||||||
|
}
|
||||||
|
}
|
12
src/panels/config/hardware/show-dialog-hardware-available.ts
Normal file
12
src/panels/config/hardware/show-dialog-hardware-available.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export const loadHardwareAvailableDialog = () =>
|
||||||
|
import("./dialog-hardware-available");
|
||||||
|
|
||||||
|
export const showhardwareAvailableDialog = (element: HTMLElement): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-hardware-available",
|
||||||
|
dialogImport: loadHardwareAvailableDialog,
|
||||||
|
dialogParams: {},
|
||||||
|
});
|
||||||
|
};
|
@ -10,7 +10,8 @@ import { stopPropagation } from "../../../common/dom/stop_propagation";
|
|||||||
import { currencies } from "../../../components/currency-datalist";
|
import { currencies } from "../../../components/currency-datalist";
|
||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-formfield";
|
import "../../../components/ha-formfield";
|
||||||
import { HaRadio } from "../../../components/ha-radio";
|
import "../../../components/ha-radio";
|
||||||
|
import type { HaRadio } from "../../../components/ha-radio";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||||
|
@ -1465,7 +1465,22 @@
|
|||||||
"internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate."
|
"internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate."
|
||||||
},
|
},
|
||||||
"hardware": {
|
"hardware": {
|
||||||
"caption": "Hardware"
|
"caption": "Hardware",
|
||||||
|
"available_hardware": {
|
||||||
|
"failed_to_get": "Failed to get available hardware",
|
||||||
|
"title": "Available hardware",
|
||||||
|
"subsystem": "Subsystem",
|
||||||
|
"device_path": "Device path",
|
||||||
|
"id": "ID",
|
||||||
|
"attributes": "Attributes"
|
||||||
|
},
|
||||||
|
"reboot_host": "Reboot host",
|
||||||
|
"reboot_host_confirm": "Are you sure you want to reboot your host?",
|
||||||
|
"failed_to_reboot_host": "Failed to reboot host",
|
||||||
|
"shutdown_host": "Shutdown host",
|
||||||
|
"shutdown_host_confirm": "Are you sure you want to shutdown your host?",
|
||||||
|
"failed_to_shutdown_host": "Failed to shutdown host",
|
||||||
|
"board": "Board"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"caption": "Info",
|
"caption": "Info",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user