Improve IP configuration UI (#22320)

* Split netmask from IP address

* handle ipv6 as well

* render fix

* improved UI for IP, mask & DNS

* remove ip detail dialog

* use `nothing`

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* use ha-list-item instead of mwc

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
Petar Petrov 2024-10-21 19:49:43 +03:00 committed by GitHub
parent e2a89a55b7
commit 202bc6440b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 330 additions and 265 deletions

View File

@ -4,7 +4,7 @@ import { hassioApiResultExtractor, HassioResponse } from "./common";
interface IpConfiguration { interface IpConfiguration {
address: string[]; address: string[];
gateway: string; gateway: string | null;
method: "disabled" | "static" | "auto"; method: "disabled" | "static" | "auto";
nameservers: string[]; nameservers: string[];
} }
@ -114,3 +114,65 @@ export const accesspointScan = async (
) )
); );
}; };
export const parseAddress = (address: string) => {
const [ip, cidr] = address.split("/");
return { ip, mask: cidrToNetmask(cidr, address.includes(":")) };
};
export const formatAddress = (ip: string, mask: string) =>
`${ip}/${netmaskToCidr(mask)}`;
// Helper functions
export const cidrToNetmask = (
cidr: string,
isIPv6: boolean = false
): string => {
const bits = parseInt(cidr, 10);
if (isIPv6) {
const fullMask = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff";
const numGroups = Math.floor(bits / 16);
const remainingBits = bits % 16;
const lastGroup = remainingBits
? parseInt(
"1".repeat(remainingBits) + "0".repeat(16 - remainingBits),
2
).toString(16)
: "";
return fullMask
.split(":")
.slice(0, numGroups)
.concat(lastGroup)
.concat(Array(8 - numGroups - (lastGroup ? 1 : 0)).fill("0"))
.join(":");
}
/* eslint-disable no-bitwise */
const mask = ~(2 ** (32 - bits) - 1);
return [
(mask >>> 24) & 255,
(mask >>> 16) & 255,
(mask >>> 8) & 255,
mask & 255,
].join(".");
/* eslint-enable no-bitwise */
};
export const netmaskToCidr = (netmask: string): number => {
if (netmask.includes(":")) {
// IPv6
return netmask
.split(":")
.map((group) =>
group ? (parseInt(group, 16).toString(2).match(/1/g) || []).length : 0
)
.reduce((sum, val) => sum + val, 0);
}
// IPv4
return netmask
.split(".")
.reduce(
(count, octet) =>
count + (parseInt(octet, 10).toString(2).match(/1/g) || []).length,
0
);
};

View File

@ -1,146 +0,0 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import type { NetworkInterface } from "../../../data/hassio/network";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { IPDetailDialogParams } from "./show-ip-detail-dialog";
@customElement("dialog-ip-detail")
class DialogIPDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: IPDetailDialogParams;
@state() private _interface?: NetworkInterface;
public showDialog(params: IPDetailDialogParams): void {
this._params = params;
this._interface = this._params.interface;
}
public closeDialog() {
this._params = undefined;
this._interface = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._interface) {
return nothing;
}
const ipv4 = this._interface.ipv4;
const ipv6 = this._interface.ipv6;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.dialog-ip-detail.ip_information")
)}
>
${ipv4
? html`
<div>
<h3>
${this.hass.localize("ui.dialogs.dialog-ip-detail.ipv4")}
</h3>
${ipv4.address
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.ip_address",
{ address: ipv4.address?.join(", ") }
)}
</div>`
: ""}
${ipv4.gateway
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.gateway",
{ gateway: ipv4.gateway }
)}
</div>`
: ""}
${ipv4.method
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.method",
{ method: ipv4.method }
)}
</div>`
: ""}
${ipv4.nameservers?.length
? html`
<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.nameservers",
{ nameservers: ipv4.nameservers?.join(", ") }
)}
</div>
`
: ""}
</div>
`
: ""}
${ipv6
? html`
<div>
<h3>
${this.hass.localize("ui.dialogs.dialog-ip-detail.ipv6")}
</h3>
${ipv6.address
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.ip_address",
{ address: ipv6.address?.join(", ") }
)}
</div>`
: ""}
${ipv6.gateway
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.gateway",
{ gateway: ipv6.gateway }
)}
</div>`
: ""}
${ipv6.method
? html`<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.method",
{ method: ipv6.method }
)}
</div>`
: ""}
${ipv6.nameservers?.length
? html`
<div>
${this.hass.localize(
"ui.dialogs.dialog-ip-detail.nameservers",
{ nameservers: ipv6.nameservers?.join(", ") }
)}
</div>
`
: ""}
</div>
`
: ""}
</ha-dialog>
`;
}
static styles: CSSResultGroup = haStyleDialog;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ip-detail": DialogIPDetail;
}
}

View File

@ -1,19 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { NetworkInterface } from "../../../data/hassio/network";
export interface IPDetailDialogParams {
interface?: NetworkInterface;
}
export const loadIPDetailDialog = () => import("./dialog-ip-detail");
export const showIPDetailDialog = (
element: HTMLElement,
dialogParams: IPDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ip-detail",
dialogImport: loadIPDetailDialog,
dialogParams,
});
};

View File

@ -1,13 +1,11 @@
import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
import "@material/mwc-tab-bar"; import "@material/mwc-tab-bar";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDeleteOutline, mdiPlus, mdiMenuDown } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache"; import { cache } from "lit/directives/cache";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
@ -16,6 +14,7 @@ import "../../../components/ha-formfield";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-password-field"; import "../../../components/ha-password-field";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import "../../../components/ha-list-item";
import type { HaRadio } from "../../../components/ha-radio"; import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield";
@ -24,7 +23,9 @@ import {
AccessPoints, AccessPoints,
accesspointScan, accesspointScan,
fetchNetworkInfo, fetchNetworkInfo,
formatAddress,
NetworkInterface, NetworkInterface,
parseAddress,
updateNetworkInterface, updateNetworkInterface,
WifiConfiguration, WifiConfiguration,
} from "../../../data/hassio/network"; } from "../../../data/hassio/network";
@ -33,10 +34,26 @@ import {
showConfirmationDialog, showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showIPDetailDialog } from "./show-ip-detail-dialog";
const IP_VERSIONS = ["ipv4", "ipv6"]; const IP_VERSIONS = ["ipv4", "ipv6"];
const PREDEFINED_DNS = {
ipv4: {
Cloudflare: ["1.1.1.1", "1.0.0.1"],
Google: ["8.8.8.8", "8.8.4.4"],
Quad9: ["9.9.9.9", "149.112.112.112"],
NextDNS: ["45.90.28.0", "45.90.30.0"],
AdGuard: ["94.140.14.140", "94.140.14.141"],
},
ipv6: {
Cloudflare: ["2606:4700:4700::1111", "2606:4700:4700::1001"],
Google: ["2001:4860:4860::8888", "2001:4860:4860::8844"],
Quad9: ["2620:fe::fe", "2620:fe::9"],
NextDNS: ["2a05:d014:a000::", "2a05:d014:a001::"],
AdGuard: ["94.140.14.140", "94.140.14.141"],
},
};
@customElement("supervisor-network") @customElement("supervisor-network")
export class HassioNetwork extends LitElement { export class HassioNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -57,6 +74,8 @@ export class HassioNetwork extends LitElement {
@state() private _wifiConfiguration?: WifiConfiguration; @state() private _wifiConfiguration?: WifiConfiguration;
@state() private _dnsMenuOpen = false;
protected firstUpdated() { protected firstUpdated() {
this._fetchNetworkInfo(); this._fetchNetworkInfo();
} }
@ -121,7 +140,7 @@ export class HassioNetwork extends LitElement {
)} )}
</p>` </p>`
: ""} : ""}
<mwc-button <ha-button
class="scan" class="scan"
@click=${this._scanForAP} @click=${this._scanForAP}
.disabled=${this._scanning} .disabled=${this._scanning}
@ -132,7 +151,7 @@ export class HassioNetwork extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.network.supervisor.scan_ap" "ui.panel.config.network.supervisor.scan_ap"
)} )}
</mwc-button> </ha-button>
${this._accessPoints && ${this._accessPoints &&
this._accessPoints.accesspoints && this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0 this._accessPoints.accesspoints.length !== 0
@ -142,7 +161,7 @@ export class HassioNetwork extends LitElement {
.filter((ap) => ap.ssid) .filter((ap) => ap.ssid)
.map( .map(
(ap) => html` (ap) => html`
<mwc-list-item <ha-list-item
twoline twoline
@click=${this._selectAP} @click=${this._selectAP}
.activated=${ap.ssid === .activated=${ap.ssid ===
@ -157,7 +176,7 @@ export class HassioNetwork extends LitElement {
)}: )}:
${ap.signal} ${ap.signal}
</span> </span>
</mwc-list-item> </ha-list-item>
` `
)} )}
</mwc-list> </mwc-list>
@ -240,35 +259,15 @@ export class HassioNetwork extends LitElement {
: ""} : ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}> <ha-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing ${this._processing
? html`<ha-circular-progress indeterminate size="small"> ? html`<ha-circular-progress indeterminate size="small">
</ha-circular-progress>` </ha-circular-progress>`
: this.hass.localize("ui.common.save")} : this.hass.localize("ui.common.save")}
</mwc-button> </ha-button>
<ha-button-menu @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${"ui.common.menu"}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item
>${this.hass.localize(
"ui.panel.config.network.ip_information"
)}</mwc-list-item
>
</ha-button-menu>
</div>`; </div>`;
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
showIPDetailDialog(this, { interface: this._interface });
break;
}
}
private _selectAP(event) { private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap; this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true; this._dirty = true;
@ -295,6 +294,11 @@ export class HassioNetwork extends LitElement {
} }
private _renderIPConfiguration(version: string) { private _renderIPConfiguration(version: string) {
const nameservers = this._interface![version]?.nameservers || [];
if (nameservers.length === 0) {
nameservers.push(""); // always show input
}
const disableInputs = this._interface![version]?.method === "auto";
return html` return html`
<ha-expansion-panel <ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`} .header=${`IPv${version.charAt(version.length - 1)}`}
@ -345,69 +349,146 @@ export class HassioNetwork extends LitElement {
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
</div> </div>
${this._interface![version].method === "static" ${["static", "auto"].includes(this._interface![version].method)
? html` ? html`
<ha-textfield ${this._interface![version].address.map(
id="address" (address: string, index: number) => {
.label=${this.hass.localize( const { ip, mask } = parseAddress(address);
"ui.panel.config.network.supervisor.ip_netmask" return html`
)} <div class="address-row">
.version=${version} <ha-textfield
.value=${this._toString(this._interface![version].address)} id="address"
@change=${this._handleInputValueChanged} .label=${this.hass.localize(
> "ui.panel.config.network.supervisor.ip"
</ha-textfield> )}
.version=${version}
.value=${ip}
.index=${index}
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
<ha-textfield
id="netmask"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.netmask"
)}
.version=${version}
.value=${mask}
.index=${index}
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
${this._interface![version].address.length > 1 &&
!disableInputs
? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDeleteOutline}
.version=${version}
.index=${index}
@click=${this._removeAddress}
></ha-icon-button>
`
: nothing}
</div>
`;
}
)}
${!disableInputs
? html`
<ha-button
@click=${this._addAddress}
.version=${version}
class="add-address"
>
${this.hass.localize(
"ui.panel.config.network.supervisor.add_address"
)}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
`
: nothing}
<ha-textfield <ha-textfield
id="gateway" id="gateway"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.network.supervisor.gateway" "ui.panel.config.network.supervisor.gateway"
)} )}
.version=${version} .version=${version}
.value=${this._interface![version].gateway} .value=${this._interface![version].gateway || ""}
@change=${this._handleInputValueChanged} @change=${this._handleInputValueChanged}
.disabled=${disableInputs}
> >
</ha-textfield> </ha-textfield>
<ha-textfield <div class="nameservers">
id="nameservers" ${nameservers.map(
.label=${this.hass.localize( (nameserver: string, index: number) => html`
"ui.panel.config.network.supervisor.dns_servers" <div class="address-row">
<ha-textfield
id="nameserver"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.dns_server"
)}
.version=${version}
.value=${nameserver}
.index=${index}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
${this._interface![version].nameservers?.length > 1
? html`
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDeleteOutline}
.version=${version}
.index=${index}
@click=${this._removeNameserver}
></ha-icon-button>
`
: nothing}
</div>
`
)} )}
</div>
<ha-button-menu
@opened=${this._handleDNSMenuOpened}
@closed=${this._handleDNSMenuClosed}
.version=${version} .version=${version}
.value=${this._toString(this._interface![version].nameservers)} class="add-nameserver"
@change=${this._handleInputValueChanged}
> >
</ha-textfield> <ha-button slot="trigger">
${this.hass.localize(
"ui.panel.config.network.supervisor.add_dns_server"
)}
<ha-svg-icon
slot="icon"
.path=${this._dnsMenuOpen ? mdiMenuDown : mdiPlus}
></ha-svg-icon>
</ha-button>
${Object.entries(PREDEFINED_DNS[version]).map(
([name, addresses]) => html`
<ha-list-item
@click=${this._addPredefinedDNS}
.version=${version}
.addresses=${addresses}
>
${name}
</ha-list-item>
`
)}
<ha-list-item @click=${this._addCustomDNS} .version=${version}>
${this.hass.localize(
"ui.panel.config.network.supervisor.custom_dns"
)}
</ha-list-item>
</ha-button-menu>
` `
: ""} : ""}
</ha-expansion-panel> </ha-expansion-panel>
`; `;
} }
_toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
_toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
}
private async _updateNetwork() { private async _updateNetwork() {
this._processing = true; this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {}; let interfaceOptions: Partial<NetworkInterface> = {};
@ -419,9 +500,13 @@ export class HassioNetwork extends LitElement {
if (this._interface![version]?.method === "static") { if (this._interface![version]?.method === "static") {
interfaceOptions[version] = { interfaceOptions[version] = {
...interfaceOptions[version], ...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address), address: this._interface![version]?.address?.filter(
(address: string) => address.trim()
),
gateway: this._interface![version]?.gateway, gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers), nameservers: this._interface![version]?.nameservers?.filter(
(ns: string) => ns.trim()
),
}; };
} }
}); });
@ -515,16 +600,28 @@ export class HassioNetwork extends LitElement {
const version = (ev.target as any).version as "ipv4" | "ipv6"; const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = source.id; const id = source.id;
if ( if (!value || !this._interface?.[version]) {
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
return; return;
} }
this._dirty = true; this._dirty = true;
this._interface[version]![id] = value; if (id === "address") {
const index = (ev.target as any).index as number;
const { mask } = parseAddress(value);
this._interface[version]!.address![index] = formatAddress(value, mask);
this.requestUpdate("_interface");
} else if (id === "netmask") {
const index = (ev.target as any).index as number;
const { ip } = parseAddress(this._interface![version]!.address![index]);
this._interface[version]!.address![index] = formatAddress(ip, value);
this.requestUpdate("_interface");
} else if (id === "nameserver") {
const index = (ev.target as any).index as number;
this._interface[version]!.nameservers![index] = value;
this.requestUpdate("_interface");
} else {
this._interface[version]![id] = value;
}
} }
private _handleInputValueChangedWifi(ev: Event): void { private _handleInputValueChangedWifi(ev: Event): void {
@ -543,6 +640,64 @@ export class HassioNetwork extends LitElement {
this._wifiConfiguration![id] = value; this._wifiConfiguration![id] = value;
} }
private _addAddress(ev: Event): void {
const version = (ev.target as any).version as "ipv4" | "ipv6";
this._interface![version]!.address!.push(
version === "ipv4" ? "0.0.0.0/24" : "::/64"
);
this._dirty = true;
this.requestUpdate("_interface");
}
private _removeAddress(ev: Event): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";
this._interface![version]!.address!.splice(index, 1);
this._dirty = true;
this.requestUpdate("_interface");
}
private _handleDNSMenuOpened() {
this._dnsMenuOpen = true;
}
private _handleDNSMenuClosed() {
this._dnsMenuOpen = false;
}
private _addPredefinedDNS(ev: Event) {
const source = ev.target as any;
const version = source.version as "ipv4" | "ipv6";
const addresses = source.addresses as string[];
if (!this._interface![version]!.nameservers) {
this._interface![version]!.nameservers = [];
}
this._interface![version]!.nameservers!.push(...addresses);
this._dirty = true;
this.requestUpdate("_interface");
}
private _addCustomDNS(ev: Event) {
const source = ev.target as any;
const version = source.version as "ipv4" | "ipv6";
if (!this._interface![version]!.nameservers) {
this._interface![version]!.nameservers = [];
}
this._interface![version]!.nameservers!.push("");
this._dirty = true;
this.requestUpdate("_interface");
}
private _removeNameserver(ev: Event): void {
const source = ev.target as any;
const index = source.index as number;
const version = source.version as "ipv4" | "ipv6";
this._interface![version]!.nameservers!.splice(index, 1);
this._dirty = true;
this.requestUpdate("_interface");
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
css` css`
@ -557,11 +712,11 @@ export class HassioNetwork extends LitElement {
padding: 20px 24px; padding: 20px 24px;
} }
mwc-button.warning { ha-button.warning {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
mwc-button.scan { ha-button.scan {
margin-left: 8px; margin-left: 8px;
margin-inline-start: 8px; margin-inline-start: 8px;
margin-inline-end: initial; margin-inline-end: initial;
@ -574,10 +729,24 @@ export class HassioNetwork extends LitElement {
display: block; display: block;
margin-top: 16px; margin-top: 16px;
} }
ha-expansion-panel ha-textfield:last-child { .address-row {
margin-bottom: 16px; display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
} }
mwc-list-item { .address-row ha-textfield {
flex: 1;
}
.address-row ha-icon-button {
--mdc-icon-button-size: 36px;
margin-top: 16px;
}
.add-address,
.add-nameserver {
margin-top: 16px;
}
ha-list-item {
--mdc-list-side-padding: 10px; --mdc-list-side-padding: 10px;
} }
.card-actions { .card-actions {
@ -586,6 +755,9 @@ export class HassioNetwork extends LitElement {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
ha-expansion-panel > :last-child {
margin-bottom: 16px;
}
`, `,
]; ];
} }

View File

@ -1708,15 +1708,6 @@
"message_placeholder": "Enter a sentence to speak.", "message_placeholder": "Enter a sentence to speak.",
"play": "Play" "play": "Play"
}, },
"dialog-ip-detail": {
"ip_information": "[%key:ui::panel::config::network::ip_information%]",
"ipv4": "IPv4",
"ipv6": "IPv6",
"ip_address": "IP Address: {address}",
"gateway": "Gateway: {gateway}",
"method": "Method: {method}",
"nameservers": "Name Servers: {nameservers}"
},
"update_backup": { "update_backup": {
"title": "Create backup?", "title": "Create backup?",
"text": "This will create a backup before installing.", "text": "This will create a backup before installing.",
@ -5240,9 +5231,13 @@
"static": "Static", "static": "Static",
"auto": "Automatic", "auto": "Automatic",
"disabled": "Disabled", "disabled": "Disabled",
"ip_netmask": "IP address/Netmask", "ip": "IP address",
"netmask": "Netmask",
"add_address": "Add address",
"gateway": "Gateway address", "gateway": "Gateway address",
"dns_servers": "DNS Servers", "dns_server": "DNS Server",
"add_dns_server": "Add DNS Server",
"custom_dns": "Custom",
"unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", "unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
"failed_to_change": "Failed to change network settings", "failed_to_change": "Failed to change network settings",
"hostname": { "hostname": {
@ -7726,6 +7721,7 @@
"auto": "Automatic", "auto": "Automatic",
"disabled": "Disabled", "disabled": "Disabled",
"ip_netmask": "IP address/netmask", "ip_netmask": "IP address/netmask",
"netmask": "Netmask",
"gateway": "Gateway address", "gateway": "Gateway address",
"dns_servers": "DNS servers", "dns_servers": "DNS servers",
"unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", "unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?",