mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-28 16:00:38 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adfc5e9c46 | ||
|
|
35a3ff6bad | ||
|
|
3053721dca | ||
|
|
9a4fdc44ad | ||
|
|
1863c3c28f | ||
|
|
46efe4d9ac | ||
|
|
6c1f7662f5 | ||
|
|
e9cfedc822 | ||
|
|
09f310a80a | ||
|
|
ab5ed3309d | ||
|
|
8b7a27eb36 | ||
|
|
68b6575913 | ||
|
|
a733e9f4ee | ||
|
|
e85f4bf824 | ||
|
|
555ebd887d | ||
|
|
1aca7afa0f | ||
|
|
9410690121 | ||
|
|
c9250afcb3 | ||
|
|
967ede6390 | ||
|
|
6a908a7fcb |
@@ -32,8 +32,13 @@ import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import "../animation/ha-fade-in";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../skeleton/ha-skeleton-icon";
|
||||
import "../skeleton/ha-skeleton-text";
|
||||
import "../ha-svg-icon";
|
||||
import "../input/ha-input-search";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
@@ -111,6 +116,9 @@ export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
|
||||
|
||||
// Default row height, used to fill the viewport with skeleton rows.
|
||||
const ROW_HEIGHT = 52;
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends LitElement {
|
||||
@state()
|
||||
@@ -143,6 +151,11 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public noDataText?: string;
|
||||
|
||||
/**
|
||||
* Error to show below the column headings, with a retry action, when loading the table's data failed.
|
||||
*/
|
||||
@property({ attribute: false }) public loadError?: string;
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
@@ -518,35 +531,110 @@ export class HaDataTable extends LitElement {
|
||||
})}
|
||||
</slot>
|
||||
</div>
|
||||
${
|
||||
this.loadError
|
||||
? html`<ha-alert class="load-error" alert-type="error">
|
||||
${this.loadError}
|
||||
<ha-button
|
||||
slot="action"
|
||||
appearance="plain"
|
||||
@click=${this._retryLoad}
|
||||
>
|
||||
${this._i18n?.localize?.("ui.components.data-table.retry")}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!this._filteredData?.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div
|
||||
class="mdc-data-table__cell grows center"
|
||||
role="cell"
|
||||
>
|
||||
${
|
||||
this.loading ||
|
||||
!this._filteredData ||
|
||||
(this.data.length && !this._filteredDataSourceLength)
|
||||
? this._i18n?.localize?.("ui.common.loading") ||
|
||||
? this.loading ||
|
||||
!this._filteredData ||
|
||||
(this.data.length && !this._filteredDataSourceLength)
|
||||
? html`
|
||||
<div class="mdc-data-table__content" role="row">
|
||||
<ha-fade-in .duration=${300} easing="ease-in">
|
||||
<div role="cell">
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-label=${
|
||||
this._i18n?.localize?.("ui.common.loading") ||
|
||||
"Loading"
|
||||
: this.data.length
|
||||
? this._i18n?.localize?.(
|
||||
"ui.components.data-table.no_match_filter"
|
||||
) || "No rows matching current filters"
|
||||
: this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
${Array.from(
|
||||
{
|
||||
length: this.autoHeight
|
||||
? 1
|
||||
: Math.ceil(window.innerHeight / ROW_HEIGHT),
|
||||
},
|
||||
() => html`
|
||||
<div class="mdc-data-table__row">
|
||||
${
|
||||
this.selectable
|
||||
? html`<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
></div>`
|
||||
: nothing
|
||||
}
|
||||
${Object.entries(columns).map(
|
||||
([key, column]) =>
|
||||
(this.narrow &&
|
||||
!column.main &&
|
||||
!column.showNarrow) ||
|
||||
!this._isColumnVisible(key, column)
|
||||
? nothing
|
||||
: html`
|
||||
<div
|
||||
class="mdc-data-table__cell ${classMap(
|
||||
this._cellClasses(column)
|
||||
)}"
|
||||
style=${styleMap(this._cellStyles(column))}
|
||||
>
|
||||
${
|
||||
column.type === "icon"
|
||||
? html`<ha-skeleton-icon></ha-skeleton-icon>`
|
||||
: column.type ===
|
||||
"icon-button" ||
|
||||
column.type ===
|
||||
"overflow-menu"
|
||||
? nothing
|
||||
: html`<ha-skeleton-text></ha-skeleton-text>`
|
||||
}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-fade-in>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
`
|
||||
: this.loadError
|
||||
? nothing
|
||||
: html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div
|
||||
class="mdc-data-table__cell grows center"
|
||||
role="cell"
|
||||
>
|
||||
${
|
||||
this.data.length
|
||||
? this._i18n?.localize?.(
|
||||
"ui.components.data-table.no_match_filter"
|
||||
) || "No rows matching current filters"
|
||||
: this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
@@ -640,22 +728,8 @@ export class HaDataTable extends LitElement {
|
||||
@mouseover=${this._setTitle}
|
||||
@focus=${this._setTitle}
|
||||
role=${column.main ? "rowheader" : "cell"}
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--flex": column.type === "flex",
|
||||
"mdc-data-table__cell--numeric": column.type === "numeric",
|
||||
"mdc-data-table__cell--icon": column.type === "icon",
|
||||
"mdc-data-table__cell--icon-button":
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${styleMap({
|
||||
minWidth: column.minWidth,
|
||||
maxWidth: column.maxWidth,
|
||||
flex: column.flex || 1,
|
||||
})}
|
||||
class="mdc-data-table__cell ${classMap(this._cellClasses(column))}"
|
||||
style=${styleMap(this._cellStyles(column))}
|
||||
>
|
||||
${
|
||||
column.template
|
||||
@@ -718,6 +792,22 @@ export class HaDataTable extends LitElement {
|
||||
private _hasCellValue = (value: unknown): boolean =>
|
||||
value !== undefined && value !== null && value !== "" && value !== nothing;
|
||||
|
||||
private _cellClasses = (column: DataTableColumnData) => ({
|
||||
"mdc-data-table__cell--flex": column.type === "flex",
|
||||
"mdc-data-table__cell--numeric": column.type === "numeric",
|
||||
"mdc-data-table__cell--icon": column.type === "icon",
|
||||
"mdc-data-table__cell--icon-button": column.type === "icon-button",
|
||||
"mdc-data-table__cell--overflow-menu": column.type === "overflow-menu",
|
||||
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
});
|
||||
|
||||
private _cellStyles = (column: DataTableColumnData) => ({
|
||||
minWidth: column.minWidth,
|
||||
maxWidth: column.maxWidth,
|
||||
flex: column.flex || 1,
|
||||
});
|
||||
|
||||
private async _sortFilterData() {
|
||||
const startTime = new Date().getTime();
|
||||
const timeBetweenUpdate = startTime - this._lastUpdate;
|
||||
@@ -1071,6 +1161,10 @@ export class HaDataTable extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _retryLoad() {
|
||||
fireEvent(this, "retry-load");
|
||||
}
|
||||
|
||||
private async _calcTableHeight() {
|
||||
if (this.autoHeight) {
|
||||
return;
|
||||
@@ -1210,6 +1304,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__header-row {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow: auto;
|
||||
@@ -1267,6 +1362,8 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
@@ -1274,6 +1371,13 @@ export class HaDataTable extends LitElement {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-alert.load-error {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: var(--ha-space-2) var(--ha-space-4);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
font-family: var(--ha-font-family-body);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
@@ -1330,7 +1434,8 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__cell--icon:first-child ha-svg-icon,
|
||||
.mdc-data-table__cell--icon:first-child ha-state-icon,
|
||||
.mdc-data-table__cell--icon:first-child ha-domain-icon,
|
||||
.mdc-data-table__cell--icon:first-child ha-service-icon {
|
||||
.mdc-data-table__cell--icon:first-child ha-service-icon,
|
||||
.mdc-data-table__cell--icon:first-child ha-skeleton-icon {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
@@ -1522,7 +1627,8 @@ export class HaDataTable extends LitElement {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.scroller {
|
||||
height: calc(100% - 57px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: overlay !important;
|
||||
}
|
||||
|
||||
@@ -1579,6 +1685,7 @@ declare global {
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"retry-load": undefined;
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
|
||||
@@ -160,6 +160,12 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
*/
|
||||
@property({ type: Boolean }) public loading = false;
|
||||
|
||||
/**
|
||||
* Error to show below the column headings, with a retry action, when loading the table's data failed.
|
||||
* @type {String}
|
||||
*/
|
||||
@property({ attribute: false }) public loadError?: string;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
/**
|
||||
@@ -522,6 +528,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.data=${this.data}
|
||||
.loading=${this.loading}
|
||||
.noDataText=${this.noDataText}
|
||||
.loadError=${this.loadError}
|
||||
.filter=${this.filter}
|
||||
.selectable=${this._selectMode}
|
||||
.id=${this.id}
|
||||
|
||||
@@ -37,6 +37,10 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
|
||||
@state() public _applicationCredentials: ApplicationCredential[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -154,10 +158,19 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._getApplicationCredentials(
|
||||
this._applicationCredentials,
|
||||
this.hass.localize
|
||||
)}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.load_failed"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchApplicationCredentials}
|
||||
has-fab
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@@ -278,7 +291,21 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchApplicationCredentials() {
|
||||
this._applicationCredentials = await fetchApplicationCredentials(this.hass);
|
||||
try {
|
||||
this._applicationCredentials = await fetchApplicationCredentials(
|
||||
this.hass
|
||||
);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryFetchApplicationCredentials() {
|
||||
this._loading = true;
|
||||
this._fetchApplicationCredentials();
|
||||
}
|
||||
|
||||
private _addApplicationCredential() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeFloorName } from "../../../common/entity/compute_floor_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
@@ -42,6 +43,8 @@ import type {
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/skeleton/ha-skeleton-icon";
|
||||
import "../../../components/skeleton/ha-skeleton-text";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -115,13 +118,15 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false }) public entries!: ConfigEntry[];
|
||||
@property({ attribute: false }) public entries?: ConfigEntry[];
|
||||
|
||||
@property({ attribute: false }) public entriesFailed = false;
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entities: EntityRegistryEntry[] = [];
|
||||
entities?: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: false }) public manifests!: IntegrationManifest[];
|
||||
|
||||
@@ -238,13 +243,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
this._filters = this._storageFilters;
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
if (changedProps.has("_selected")) {
|
||||
if (changedProps.has("_selected") || changedProps.has("entries")) {
|
||||
this._selectedCanDelete = this._selected.filter((d) => {
|
||||
const device = this.hass.devices[d];
|
||||
const entries = device.config_entries;
|
||||
return entries.some(
|
||||
(entryId) =>
|
||||
this.entries.find((e) => e.entry_id === entryId)
|
||||
this.entries?.find((e) => e.entry_id === entryId)
|
||||
?.supports_remove_device
|
||||
);
|
||||
});
|
||||
@@ -307,6 +312,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _reloadConfigEntries() {
|
||||
fireEvent(this, "reload-config-entries");
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
if (!this._fromUrl) {
|
||||
@@ -317,8 +326,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
private _devicesAndFilterDomains = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistant["devices"],
|
||||
entries: ConfigEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
entries: ConfigEntry[] | undefined,
|
||||
entriesFailed: boolean,
|
||||
entities: EntityRegistryEntry[] = [],
|
||||
areas: HomeAssistant["areas"],
|
||||
manifests: IntegrationManifest[],
|
||||
filters: DataTableFilters,
|
||||
@@ -346,7 +356,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
|
||||
const entryLookup: Record<string, ConfigEntry> = {};
|
||||
for (const entry of entries) {
|
||||
|
||||
for (const entry of entries ?? []) {
|
||||
entryLookup[entry.entry_id] = entry;
|
||||
}
|
||||
|
||||
@@ -371,7 +382,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
)
|
||||
);
|
||||
|
||||
const configEntries = entries.filter(
|
||||
const configEntries = (entries ?? []).filter(
|
||||
(entry) =>
|
||||
entry.entry_id &&
|
||||
(filter.value as string[]).includes(entry.entry_id)
|
||||
@@ -412,7 +423,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
const entryIds = entries
|
||||
const entryIds = (entries ?? [])
|
||||
.filter((entry) =>
|
||||
(filter.value as string[]).includes(entry.domain)
|
||||
)
|
||||
@@ -531,16 +542,21 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
|
||||
area: areaName,
|
||||
floor: floorName,
|
||||
integration: deviceEntries.length
|
||||
? deviceEntries
|
||||
.map(
|
||||
(entry) =>
|
||||
localize(`component.${entry.domain}.title`) || entry.domain
|
||||
)
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.no_integration"
|
||||
),
|
||||
integration: !entries
|
||||
? localize("ui.common.loading")
|
||||
: entriesFailed
|
||||
? `<${localize("ui.panel.config.devices.data_table.unknown")}>`
|
||||
: deviceEntries.length
|
||||
? deviceEntries
|
||||
.map(
|
||||
(entry) =>
|
||||
localize(`component.${entry.domain}.title`) ||
|
||||
entry.domain
|
||||
)
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.devices.data_table.no_integration"
|
||||
),
|
||||
domains: deviceEntries.map((entry) => entry.domain),
|
||||
parent_device_name: parentDevice
|
||||
? computeDeviceNameDisplay(
|
||||
@@ -599,21 +615,23 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: device.domains[0],
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`
|
||||
: "",
|
||||
!this.entries
|
||||
? html`<ha-skeleton-icon></ha-skeleton-icon>`
|
||||
: device.domains.length
|
||||
? html`<img
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: device.domains[0],
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
name: {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
@@ -642,7 +660,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
.labels=${device.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: nothing
|
||||
: device.labels.length && !this._labels
|
||||
? html`<ha-skeleton-text></ha-skeleton-text>`
|
||||
: nothing
|
||||
}
|
||||
`,
|
||||
},
|
||||
@@ -654,6 +674,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
minWidth: "120px",
|
||||
template: (device) =>
|
||||
!this.entries
|
||||
? html`<ha-skeleton-text></ha-skeleton-text>`
|
||||
: device.integration,
|
||||
},
|
||||
device_family_name: {
|
||||
title: localize("ui.panel.config.devices.data_table.parent_device"),
|
||||
@@ -697,6 +721,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
minWidth: "101px",
|
||||
valueColumn: "battery_level",
|
||||
template: (device) => {
|
||||
if (!this.entities) {
|
||||
return html`<ha-skeleton-text></ha-skeleton-text>`;
|
||||
}
|
||||
const batteryEntityPair = device.battery_entity;
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
@@ -831,6 +858,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
const { devicesOutput } = this._devicesAndFilterDomains(
|
||||
this.hass.devices,
|
||||
this.entries,
|
||||
this.entriesFailed,
|
||||
this.entities,
|
||||
this.hass.areas,
|
||||
this.manifests,
|
||||
@@ -856,6 +884,14 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${devicesOutput}
|
||||
.loadError=${
|
||||
this.entriesFailed
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.devices.config_entries_load_failed"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._reloadConfigEntries}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@@ -903,10 +939,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
"ui.panel.config.devices.filtering_by_config_entry"
|
||||
)}
|
||||
${
|
||||
this.entries?.find(
|
||||
(entry) =>
|
||||
entry.entry_id === this._filters.config_entry!.value![0]
|
||||
)?.title || this._filters.config_entry.value[0]
|
||||
!this.entries
|
||||
? html`<ha-skeleton-text></ha-skeleton-text>`
|
||||
: this.entries.find(
|
||||
(entry) =>
|
||||
entry.entry_id ===
|
||||
this._filters.config_entry!.value![0]
|
||||
)?.title || this._filters.config_entry.value[0]
|
||||
}${
|
||||
this._filters.config_entry.value.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry?.value) &&
|
||||
@@ -1118,6 +1157,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
this._devicesAndFilterDomains(
|
||||
this.hass.devices,
|
||||
this.entries,
|
||||
this.entriesFailed,
|
||||
this.entities,
|
||||
this.hass.areas,
|
||||
this.manifests,
|
||||
@@ -1357,6 +1397,9 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-alert ha-skeleton-text {
|
||||
--ha-skeleton-text-width: 100px;
|
||||
}
|
||||
ha-dropdown::part(menu),
|
||||
ha-dropdown::part(submenu) {
|
||||
--auto-size-available-width: calc(50vw - var(--ha-space-4));
|
||||
|
||||
@@ -31,10 +31,25 @@ class HaConfigDevices extends HassRouterPage {
|
||||
},
|
||||
};
|
||||
|
||||
@state() private _configEntries: ConfigEntry[] = [];
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@state() private _configEntriesFailed = false;
|
||||
|
||||
@state() private _manifests: IntegrationManifest[] = [];
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("reload-config-entries", this._reloadConfigEntries);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
"reload-config-entries",
|
||||
this._reloadConfigEntries
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
@@ -48,9 +63,12 @@ class HaConfigDevices extends HassRouterPage {
|
||||
|
||||
if (this._currentPage === "device") {
|
||||
pageEl.deviceId = this.routeTail.path.substr(1);
|
||||
pageEl.entries = this._configEntries ?? [];
|
||||
} else {
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.entriesFailed = this._configEntriesFailed;
|
||||
}
|
||||
|
||||
pageEl.entries = this._configEntries;
|
||||
pageEl.manifests = this._manifests;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
@@ -59,17 +77,39 @@ class HaConfigDevices extends HassRouterPage {
|
||||
|
||||
private async _loadData() {
|
||||
await Promise.all([
|
||||
getConfigEntries(this.hass).then((configEntries) => {
|
||||
this._configEntries = configEntries;
|
||||
}),
|
||||
fetchIntegrationManifests(this.hass).then((manifests) => {
|
||||
this._manifests = manifests;
|
||||
}),
|
||||
this._loadConfigEntries(),
|
||||
fetchIntegrationManifests(this.hass)
|
||||
.then((manifests) => {
|
||||
this._manifests = manifests;
|
||||
})
|
||||
.catch(() => {
|
||||
// The pages remain usable without integration manifests.
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
private _reloadConfigEntries = () => {
|
||||
this._loadConfigEntries();
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntriesFailed = false;
|
||||
this._configEntries = undefined;
|
||||
|
||||
try {
|
||||
this._configEntries = await getConfigEntries(this.hass);
|
||||
} catch {
|
||||
this._configEntriesFailed = true;
|
||||
this._configEntries = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"reload-config-entries": undefined;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-devices": HaConfigDevices;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ class HaConfigHardwareAll extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<HardwareDeviceRow> => ({
|
||||
name: {
|
||||
@@ -98,6 +100,7 @@ class HaConfigHardwareAll extends LitElement {
|
||||
.tabs=${hardwareTabs(this.hass)}
|
||||
clickable
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._hardware ? this._data(this._hardware) : []}
|
||||
.noDataText=${
|
||||
this._error ||
|
||||
@@ -113,6 +116,8 @@ class HaConfigHardwareAll extends LitElement {
|
||||
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
|
||||
@state() private _groups: ZHAGroup[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -114,7 +118,14 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._formattedGroups(this._groups)}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize("ui.panel.config.zha.groups.load_failed")
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchGroups}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
has-fab
|
||||
@@ -128,7 +139,19 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchGroups() {
|
||||
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
|
||||
try {
|
||||
this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryFetchGroups() {
|
||||
this._loading = true;
|
||||
this._fetchGroups();
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
|
||||
@@ -30,6 +30,10 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
|
||||
@state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
@state() private _nodeIdToDevice: Record<number, DeviceRegistryEntry> = {};
|
||||
|
||||
protected render() {
|
||||
@@ -50,7 +54,16 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
this.configEntryId
|
||||
}"
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._getData(this._provisioningEntries, this._nodeIdToDevice)}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.zwave_js.provisioned.load_failed"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchProvisioningEntries}
|
||||
>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
@@ -176,10 +189,22 @@ class ZWaveJSProvisioned extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchProvisioningEntries() {
|
||||
this._provisioningEntries = await fetchZwaveProvisioningEntries(
|
||||
this.hass!,
|
||||
this.configEntryId
|
||||
);
|
||||
try {
|
||||
this._provisioningEntries = await fetchZwaveProvisioningEntries(
|
||||
this.hass!,
|
||||
this.configEntryId
|
||||
);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryFetchProvisioningEntries() {
|
||||
this._loading = true;
|
||||
this._fetchProvisioningEntries();
|
||||
}
|
||||
|
||||
private _unprovision = async (ev) => {
|
||||
|
||||
@@ -101,6 +101,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
@state() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
@@ -409,6 +413,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards,
|
||||
this.hass.localize
|
||||
)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._getItems(
|
||||
this._dashboards,
|
||||
defaultPanel,
|
||||
@@ -426,6 +431,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.load_failed"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryGetDashboards}
|
||||
id="url_path"
|
||||
has-fab
|
||||
clickable
|
||||
@@ -476,7 +489,19 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
}
|
||||
|
||||
private async _getDashboards() {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
try {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryGetDashboards() {
|
||||
this._loading = true;
|
||||
this._getDashboards();
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: CustomEvent) {
|
||||
|
||||
@@ -58,6 +58,10 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _tags: Tag[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
private get _canWriteTags() {
|
||||
return this.hass.auth.external?.config.canWriteTag;
|
||||
}
|
||||
@@ -195,8 +199,15 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.tags}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize("ui.panel.config.tag.load_failed")
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchTags}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
has-fab
|
||||
@@ -266,7 +277,19 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _fetchTags() {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
try {
|
||||
this._tags = await fetchTags(this.hass);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryFetchTags() {
|
||||
this._loading = true;
|
||||
this._fetchTags();
|
||||
}
|
||||
|
||||
private _openWrite(tag: Tag) {
|
||||
|
||||
@@ -40,6 +40,10 @@ export class HaConfigUsers extends LitElement {
|
||||
|
||||
@state() private _users: User[] = [];
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
@storage({ key: "users-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@@ -179,7 +183,14 @@ export class HaConfigUsers extends LitElement {
|
||||
back-path="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.loading=${this._loading}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this.hass.localize("ui.panel.config.users.picker.load_failed")
|
||||
: undefined
|
||||
}
|
||||
@retry-load=${this._retryFetchUsers}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@@ -212,7 +223,16 @@ export class HaConfigUsers extends LitElement {
|
||||
);
|
||||
|
||||
private async _fetchUsers() {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
try {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
this._loadFailed = false;
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
|
||||
return;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
this._users.forEach((user) => {
|
||||
if (user.is_owner) {
|
||||
@@ -221,6 +241,11 @@ export class HaConfigUsers extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _retryFetchUsers() {
|
||||
this._loading = true;
|
||||
this._fetchUsers();
|
||||
}
|
||||
|
||||
private _editUser(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const id = ev.detail.id;
|
||||
const entry = this._users.find((user) => user.id === id);
|
||||
|
||||
@@ -1219,7 +1219,8 @@
|
||||
"show": "Show column {title}",
|
||||
"done": "Done",
|
||||
"restore": "Restore defaults"
|
||||
}
|
||||
},
|
||||
"retry": "[%key:ui::panel::app::retry%]"
|
||||
},
|
||||
"siren": {
|
||||
"more_controls": "More controls",
|
||||
@@ -3977,7 +3978,8 @@
|
||||
"required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]",
|
||||
"usage": "A tag can trigger an automation when scanned, you can use NFC tags, QR codes or any other kind of tag. Use our {companion_link} to write this tag to a programmable NFC tag or copy and print the QR code below.",
|
||||
"companion_apps": "Companion apps"
|
||||
}
|
||||
},
|
||||
"load_failed": "An unknown error occurred while loading tags."
|
||||
},
|
||||
"tools": {
|
||||
"tabs": {
|
||||
@@ -4836,7 +4838,8 @@
|
||||
"type": {
|
||||
"user_created": "User created",
|
||||
"built_in": "Built-in"
|
||||
}
|
||||
},
|
||||
"load_failed": "An unknown error occurred while loading dashboards."
|
||||
},
|
||||
"confirm_delete_title": "Delete {dashboard_title}?",
|
||||
"confirm_delete_text": "This dashboard will be permanently deleted.",
|
||||
@@ -6963,6 +6966,7 @@
|
||||
"caption": "Devices",
|
||||
"description": "Manage configured devices",
|
||||
"filtering_by_config_entry": "[%key:ui::panel::config::entities::picker::filtering_by_config_entry%]",
|
||||
"config_entries_load_failed": "An unknown error occurred while loading integrations for your devices.",
|
||||
"device_info": "{type} info",
|
||||
"edit_settings": "Edit settings",
|
||||
"restore_entity_ids": "Recreate entity IDs",
|
||||
@@ -7486,7 +7490,8 @@
|
||||
"local": "Local",
|
||||
"icon": "Icon"
|
||||
},
|
||||
"add_user": "Add user"
|
||||
"add_user": "Add user",
|
||||
"load_failed": "An unknown error occurred while loading users."
|
||||
},
|
||||
"editor": {
|
||||
"caption": "View user",
|
||||
@@ -7572,7 +7577,8 @@
|
||||
"confirm_text": "Application credentials in use by an integration may not be deleted.",
|
||||
"error_title": "Deleting application credentials failed"
|
||||
},
|
||||
"selected": "{number} selected"
|
||||
"selected": "{number} selected",
|
||||
"load_failed": "An unknown error occurred while loading application credentials."
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
@@ -7983,7 +7989,8 @@
|
||||
"delete": "Delete group",
|
||||
"endpoint": "Endpoint",
|
||||
"associated_entities": "Associated entities",
|
||||
"no_associated_entities": "This endpoint has no associated entities"
|
||||
"no_associated_entities": "This endpoint has no associated entities",
|
||||
"load_failed": "An unknown error occurred while loading groups."
|
||||
},
|
||||
"visualization": {
|
||||
"header": "Zigbee visualization",
|
||||
@@ -8409,7 +8416,8 @@
|
||||
"confirm_unprovision_title": "Remove device?",
|
||||
"confirm_unprovision_text": "This device will be permanently removed from your Z-Wave network.",
|
||||
"confirm_unprovision_text_included": "This device will be permanently removed from Home Assistant and your Z-Wave network.",
|
||||
"active": "Active"
|
||||
"active": "Active",
|
||||
"load_failed": "An unknown error occurred while loading provisioned devices."
|
||||
},
|
||||
"security_classes": {
|
||||
"None": {
|
||||
|
||||
Reference in New Issue
Block a user