Compare commits

..
4 changed files with 83 additions and 204 deletions

No files matched your search

-80
View File
@@ -1,80 +0,0 @@
import type {
DataTableFiltersValue,
DataTableFiltersValues,
} from "../../data/data_table_filters";
import { dataTableQueryParamConfig } from "./data-table-query-params";
import {
createQueryString,
decodeQueryParams,
hasQueryParams,
type QueryParamConfig,
type QueryParamValues,
type SearchParamsSource,
} from "./query-params";
// The filter-pane keys of the backups table's filters
export const BACKUP_TYPE_FILTER = "ha-filter-states";
export const BACKUP_LOCATIONS_FILTER = "backup-locations";
// The filters of the backups table. Search, sorting and grouping are kept by
// the data table, see data-table-query-params.
export const backupsFilterQueryParamConfig = {
list: ["type", "location"],
} as const satisfies QueryParamConfig;
export type BackupsFilterQueryParams = QueryParamValues<
typeof backupsFilterQueryParamConfig
>;
const list = (value: DataTableFiltersValue): string[] | undefined =>
Array.isArray(value) ? value : undefined;
export const backupsFilterQueryParams = (
filters: DataTableFiltersValues
): BackupsFilterQueryParams => ({
type: list(filters[BACKUP_TYPE_FILTER]),
location: list(filters[BACKUP_LOCATIONS_FILTER]),
});
// The filters as the URL holds them, so values that filter the same, such as
// no list and an empty one, compare equal
export const backupsFilterQueryString = (
filters: DataTableFiltersValues
): string =>
createQueryString(
backupsFilterQueryParams(filters),
backupsFilterQueryParamConfig
);
// What a link says to show, undefined when it has no table params so what was
// picked before stays. A link replaces the search and the filters, ones left
// behind would hide part of what it shows. The table applies its sorting and
// grouping.
export interface BackupsLink {
search: string;
filters: DataTableFiltersValues;
}
export const backupsLinkFromUrl = (
searchParams: SearchParamsSource
): BackupsLink | undefined => {
if (
!hasQueryParams(searchParams, backupsFilterQueryParamConfig) &&
!hasQueryParams(searchParams, dataTableQueryParamConfig)
) {
return undefined;
}
const params = decodeQueryParams(searchParams, backupsFilterQueryParamConfig);
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
return {
search: search ?? "",
filters: {
// `type=all` asks for every type, as the backups overview links do
[BACKUP_TYPE_FILTER]:
params.type?.filter((type) => type && type !== "all") ?? [],
[BACKUP_LOCATIONS_FILTER]: params.location?.filter(Boolean) ?? [],
},
};
};
@@ -3,13 +3,13 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type {
DataTableColumnContainer,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
import "../../../components/ha-help-tooltip";
@@ -52,12 +52,10 @@ export class HaConfigApplicationCredentials extends LitElement {
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@storage({
key: "application-credentials-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
private _tableSettings = new DataTableSettingsController(this, {
search: "application-credentials-table-search",
sorting: "application-credentials-table-sort",
});
@storage({
key: "application-credentials-table-column-order",
@@ -73,15 +71,6 @@ export class HaConfigApplicationCredentials extends LitElement {
})
private _activeHiddenColumns?: string[];
@state()
@storage({
storage: "sessionStorage",
key: "application-credentials-table-search",
state: true,
subscribe: false,
})
private _filter = "";
private _columns = memoizeOne(
(localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<ApplicationCredential> = {
@@ -107,6 +96,7 @@ export class HaConfigApplicationCredentials extends LitElement {
),
sortable: true,
filterable: true,
urlName: "integration",
},
actions: {
lastFixed: true,
@@ -175,13 +165,14 @@ export class HaConfigApplicationCredentials extends LitElement {
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
.initialSorting=${this._activeSorting}
.initialSorting=${this._tableSettings.sorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged}
.filter=${this._filter}
@search-changed=${this._handleSearchChange}
@sorting-changed=${this._tableSettings.sortingChanged}
.filter=${this._tableSettings.search}
@search-changed=${this._tableSettings.searchChanged}
sync-url
>
<div class="header-btns" slot="selection-bar">
${
@@ -323,19 +314,11 @@ export class HaConfigApplicationCredentials extends LitElement {
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static styles = css`
.table-header {
display: flex;
@@ -12,22 +12,12 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { navigate } from "../../../common/navigate";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
BACKUP_LOCATIONS_FILTER,
BACKUP_TYPE_FILTER,
backupsFilterQueryParamConfig,
backupsFilterQueryParams,
backupsFilterQueryString,
backupsLinkFromUrl,
} from "../../../common/url/backups-query-params";
import type {
DataTableColumnContainer,
DataTableRowData,
@@ -65,10 +55,7 @@ import {
} from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud";
import type {
DataTableFiltersValue,
DataTableFiltersValues,
} from "../../../data/data_table_filters";
import type { DataTableFiltersValues } from "../../../data/data_table_filters";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
showAlertDialog,
@@ -92,6 +79,9 @@ interface BackupRow extends DataTableRowData, BackupContent {
agent_ids: string[];
}
const TYPE_FILTER = "ha-filter-states";
const LOCATIONS_FILTER = "backup-locations";
@customElement("ha-config-backup-backups")
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -123,11 +113,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
})
private _storageFilters: DataTableFiltersValues = {};
private _tableSettings = new DataTableSettingsController(
this,
{ grouping: "backups-table-grouping" },
{ defaultGrouping: "formatted_type" }
);
private _fromUrl = false;
@storage({ key: "backups-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string = "formatted_type";
@storage({
key: "backups-table-collapsed",
@@ -145,25 +134,38 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
private _overflowBackup?: BackupRow;
// The filters are kept in the URL by the page, the search, sorting and
// grouping by the table
private _filterParams = new QueryParamsController(
this,
backupsFilterQueryParamConfig,
{
enabled: () => true,
navigated: () => this._applyLink(),
}
);
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._filters = this._storageFilters;
this._applyLink();
this._setFiltersFromUrl();
}
}
public connectedCallback() {
super.connectedCallback();
// Re-apply the type filter from the URL when the page is (re)displayed,
// e.g. when navigating back to a cached instance of this page with a
// different `type` query param.
this._setFiltersFromUrl();
window.addEventListener("location-changed", this._locationChanged);
window.addEventListener("popstate", this._popState);
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("location-changed", this._locationChanged);
window.removeEventListener("popstate", this._popState);
}
private _locationChanged = () => {
this._setFiltersFromUrl();
};
private _popState = () => {
this._setFiltersFromUrl();
};
private _columns = memoizeOne(
(
localize: LocalizeFunc,
@@ -195,7 +197,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
filterable: true,
sortable: true,
groupable: true,
urlName: "type",
},
locations: {
title: localize("ui.panel.config.backup.locations"),
@@ -333,6 +334,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
this._overflowMenu.anchorElement = undefined;
};
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
@@ -350,19 +355,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
localize: LocalizeFunc,
isHassio: boolean
): BackupRow[] => {
const typeFilter = filters[BACKUP_TYPE_FILTER];
const locationFilter = filters[BACKUP_LOCATIONS_FILTER];
const typeFilter = filters[TYPE_FILTER] as string[] | undefined;
const locationFilter = filters[LOCATIONS_FILTER] as string[] | undefined;
let filteredBackups = backups;
if (Array.isArray(typeFilter) && typeFilter.length) {
if (typeFilter?.length) {
filteredBackups = filteredBackups.filter((backup) => {
const type = computeBackupType(backup, isHassio);
return typeFilter.includes(type);
});
}
if (Array.isArray(locationFilter) && locationFilter.length) {
if (locationFilter?.length) {
filteredBackups = filteredBackups.filter((backup) =>
Object.keys(backup.agents).some((agentId) =>
locationFilter.includes(agentId)
@@ -448,21 +450,18 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._tableSettings.grouping}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.groupOrder=${this._groupOrder(
this._tableSettings.grouping,
this._activeGrouping,
this.hass.localize,
isHassio
)}
@grouping-changed=${this._tableSettings.groupingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@selection-changed=${this._handleSelectionChanged}
@clear-filter=${this._clearFilter}
.route=${this.route}
sync-url
.filter=${this._tableSettings.search}
@search-changed=${this._tableSettings.searchChanged}
@row-click=${this._showBackupDetails}
.columns=${this._columns(this.hass.localize, maxDisplayedAgents)}
.data=${data}
@@ -519,7 +518,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
<ha-filter-states
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
.value=${this._filters[BACKUP_TYPE_FILTER]}
.value=${this._filters[TYPE_FILTER]}
.states=${this._states(this.hass.localize, isHassio)}
@data-table-filter-changed=${this._typeFilterChanged}
slot="filter-pane"
@@ -527,7 +526,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
></ha-filter-states>
<ha-filter-states
.label=${this.hass.localize("ui.panel.config.backup.locations")}
.value=${this._filters[BACKUP_LOCATIONS_FILTER]}
.value=${this._filters[LOCATIONS_FILTER]}
.states=${this._locations(
this.hass.localize,
this.agents,
@@ -590,58 +589,38 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
);
private _typeFilterChanged(ev) {
this._filterChanged(BACKUP_TYPE_FILTER, ev.detail.value);
this._filters = { ...this._filters, [TYPE_FILTER]: ev.detail.value };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
}
private _locationsFilterChanged(ev) {
this._filterChanged(BACKUP_LOCATIONS_FILTER, ev.detail.value);
}
private _filterChanged(type: string, value: DataTableFiltersValue) {
const filters = { ...this._filters, [type]: value };
// Only a change to what the URL holds is the user's, so a link isn't stored
if (
backupsFilterQueryString(filters) !==
backupsFilterQueryString(this._filters)
) {
this._storageFilters = { ...this._storageFilters, [type]: value };
this._filters = { ...this._filters, [LOCATIONS_FILTER]: ev.detail.value };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._filters = filters;
this._writeFilters();
}
private _clearFilter() {
this._filters = {};
this._storageFilters = {};
this._writeFilters();
if (!this._fromUrl) {
this._storageFilters = {};
}
}
// Links, like the ones on the backups overview, say in the URL what to show.
// The router keeps the page open, so a link can land on it.
private _applyLink() {
const searchParams = this._filterParams.searchParams();
const link = searchParams ? backupsLinkFromUrl(searchParams) : undefined;
// Shown, but not stored as the user's choice
if (link) {
this._tableSettings.showSearch(link.search);
this._filters = link.filters;
} else if (searchParams?.toString() === "") {
// Opened again without params, like from the sidebar. Written once the
// table has seen the URL without params, so it restores what it stored
this._filters = this._storageFilters;
queueMicrotask(() => this._writeFilters());
private _setFiltersFromUrl() {
const searchParams = new URLSearchParams(window.location.search);
const type = searchParams.get("type");
if (!type) {
return;
}
this._writeFilters();
}
private _writeFilters() {
this._filterParams.write(backupsFilterQueryParams(this._filters));
this._fromUrl = true;
this._filters = {
[TYPE_FILTER]: type === "all" ? [] : [type],
};
}
private get _needsOnboarding() {
+8 -11
View File
@@ -54,9 +54,17 @@ const helpersTableParams = {
assistants: "string?",
} satisfies Redirect["params"];
// The application credentials table reads these from the link
const applicationCredentialsTableParams = {
search: "string?",
sort: "string?",
direction: "string?",
} satisfies Redirect["params"];
export const getMyRedirects = (): Redirects => ({
application_credentials: {
redirect: "/config/application_credentials",
params: applicationCredentialsTableParams,
},
tools_assist: {
redirect: "/config/tools/assist",
@@ -413,7 +421,6 @@ export const getMyRedirects = (): Redirects => ({
backup_list: {
component: "backup",
redirect: "/config/backup/backups",
params: backupsTableParams,
},
backup_config: {
component: "backup",
@@ -528,16 +535,6 @@ export const getMyRedirects = (): Redirects => ({
const getRedirect = (path: string): Redirect | undefined =>
getMyRedirects()?.[path];
// The backups table reads these from the link
const backupsTableParams = {
search: "string?",
sort: "string?",
direction: "string?",
group: "string?",
type: "string?",
location: "string?",
} satisfies Redirect["params"];
export type ParamType = "url" | "string" | "string?";
export type Redirects = Record<string, Redirect>;