diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index d5741cafeb..a9fa5ba391 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -269,8 +269,8 @@ export class HaDataTable extends LitElement { @change=${this._handleHeaderRowCheckboxClick} .indeterminate=${this._checkedRows.length && this._checkedRows.length !== this._checkableRowsCount} - .checked=${this._checkedRows.length === - this._checkableRowsCount} + .checked=${this._checkedRows.length && + this._checkedRows.length === this._checkableRowsCount} > diff --git a/src/data/application_credential.ts b/src/data/application_credential.ts new file mode 100644 index 0000000000..0062301597 --- /dev/null +++ b/src/data/application_credential.ts @@ -0,0 +1,44 @@ +import { HomeAssistant } from "../types"; + +export interface ApplicationCredentialsConfig { + domains: string[]; +} + +export interface ApplicationCredential { + id: string; + domain: string; + client_id: string; + client_secret: string; +} + +export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) => + hass.callWS({ + type: "application_credentials/config", + }); + +export const fetchApplicationCredentials = async (hass: HomeAssistant) => + hass.callWS({ + type: "application_credentials/list", + }); + +export const createApplicationCredential = async ( + hass: HomeAssistant, + domain: string, + clientId: string, + clientSecret: string +) => + hass.callWS({ + type: "application_credentials/create", + domain, + client_id: clientId, + client_secret: clientSecret, + }); + +export const deleteApplicationCredential = async ( + hass: HomeAssistant, + applicationCredentialsId: string +) => + hass.callWS({ + type: "application_credentials/delete", + application_credentials_id: applicationCredentialsId, + }); diff --git a/src/panels/config/application_credentials/dialog-add-application-credential.ts b/src/panels/config/application_credentials/dialog-add-application-credential.ts new file mode 100644 index 0000000000..f60ea765b1 --- /dev/null +++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts @@ -0,0 +1,224 @@ +import "@material/mwc-button"; +import "@material/mwc-list/mwc-list-item"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { ComboBoxLitRenderer } from "lit-vaadin-helpers"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-combo-box"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-textfield"; +import { + fetchApplicationCredentialsConfig, + createApplicationCredential, + ApplicationCredential, +} from "../../../data/application_credential"; +import { domainToName } from "../../../data/integration"; +import { PolymerChangedEvent } from "../../../polymer-types"; +import { haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential"; + +interface Domain { + id: string; + name: string; +} + +const rowRenderer: ComboBoxLitRenderer = (item) => html` + ${item.name} +`; + +@customElement("dialog-add-application-credential") +export class DialogAddApplicationCredential extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _loading = false; + + // Error message when can't talk to server etc + @state() private _error?: string; + + @state() private _params?: AddApplicationCredentialDialogParams; + + @state() private _domain?: string; + + @state() private _clientId?: string; + + @state() private _clientSecret?: string; + + @state() private _domains?: Domain[]; + + public showDialog(params: AddApplicationCredentialDialogParams) { + this._params = params; + this._domain = ""; + this._clientId = ""; + this._clientSecret = ""; + this._error = undefined; + this._loading = false; + this._fetchConfig(); + } + + private async _fetchConfig() { + const config = await fetchApplicationCredentialsConfig(this.hass); + this._domains = config.domains.map((domain) => ({ + id: domain, + name: domainToName(this.hass.localize, domain), + })); + } + + protected render(): TemplateResult { + if (!this._params || !this._domains) { + return html``; + } + return html` + +
+ ${this._error ? html`
${this._error}
` : ""} + + + +
+ ${this._loading + ? html` +
+ +
+ ` + : html` + + ${this.hass.localize( + "ui.panel.config.application_credentials.editor.create" + )} + + `} +
+ `; + } + + public closeDialog() { + this._params = undefined; + this._domains = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private async _handleDomainPicked(ev: PolymerChangedEvent) { + const target = ev.target as any; + if (target.selectedItem) { + this._domain = target.selectedItem.id; + } + } + + private _handleValueChanged(ev: CustomEvent) { + this._error = undefined; + const name = (ev.target as any).name; + const value = (ev.target as any).value; + this[`_${name}`] = value; + } + + private async _createApplicationCredential(ev) { + ev.preventDefault(); + if (!this._domain || !this._clientId || !this._clientSecret) { + return; + } + + this._loading = true; + this._error = ""; + + let applicationCredential: ApplicationCredential; + try { + applicationCredential = await createApplicationCredential( + this.hass, + this._domain, + this._clientId, + this._clientSecret + ); + } catch (err: any) { + this._loading = false; + this._error = err.message; + return; + } + this._params!.applicationCredentialAddedCallback(applicationCredential); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-max-width: 500px; + --dialog-z-index: 10; + } + .row { + display: flex; + padding: 8px 0; + } + ha-combo-box { + display: block; + margin-bottom: 24px; + } + ha-textfield { + display: block; + margin-bottom: 24px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-add-application-credential": DialogAddApplicationCredential; + } +} diff --git a/src/panels/config/application_credentials/ha-config-application-credentials.ts b/src/panels/config/application_credentials/ha-config-application-credentials.ts new file mode 100644 index 0000000000..fa55f07681 --- /dev/null +++ b/src/panels/config/application_credentials/ha-config-application-credentials.ts @@ -0,0 +1,259 @@ +import { mdiDelete, mdiPlus } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; +import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { + DataTableColumnContainer, + SelectionChangedEvent, +} from "../../../components/data-table/ha-data-table"; +import "../../../components/data-table/ha-data-table-icon"; +import "../../../components/ha-fab"; +import "../../../components/ha-help-tooltip"; +import "../../../components/ha-svg-icon"; +import { + ApplicationCredential, + deleteApplicationCredential, + fetchApplicationCredentials, +} from "../../../data/application_credential"; +import { domainToName } from "../../../data/integration"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-tabs-subpage-data-table"; +import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; +import { HomeAssistant, Route } from "../../../types"; +import { configSections } from "../ha-panel-config"; +import { showAddApplicationCredentialDialog } from "./show-dialog-add-application-credential"; + +@customElement("ha-config-application-credentials") +export class HaConfigApplicationCredentials extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() public _applicationCredentials: ApplicationCredential[] = []; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + @property() public route!: Route; + + @state() private _selected: string[] = []; + + @query("hass-tabs-subpage-data-table", true) + private _dataTable!: HaTabsSubpageDataTable; + + private _columns = memoizeOne( + (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { + clientId: { + title: localize( + "ui.panel.config.application_credentials.picker.headers.client_id" + ), + width: "25%", + direction: "asc", + grows: true, + template: (_, entry: ApplicationCredential) => + html`${entry.client_id}`, + }, + application: { + title: localize( + "ui.panel.config.application_credentials.picker.headers.application" + ), + sortable: true, + width: "20%", + direction: "asc", + hidden: narrow, + template: (_, entry) => html`${domainToName(localize, entry.domain)}`, + }, + }; + + return columns; + } + ); + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + this._loadTranslations(); + this._fetchApplicationCredentials(); + } + + protected render() { + return html` + + ${this._selected.length + ? html` +
+

+ ${this.hass.localize( + "ui.panel.config.application_credentials.picker.selected", + "number", + this._selected.length + )} +

+
+ ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.application_credentials.picker.remove_selected.button" + )} + ` + : html` + + + + `} +
+
+ ` + : html``} + + + +
+ `; + } + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + + private _removeSelected() { + showConfirmationDialog(this, { + title: this.hass.localize( + `ui.panel.config.application_credentials.picker.remove_selected.confirm_title`, + "number", + this._selected.length + ), + text: this.hass.localize( + "ui.panel.config.application_credentials.picker.remove_selected.confirm_text" + ), + confirmText: this.hass.localize("ui.common.remove"), + dismissText: this.hass.localize("ui.common.cancel"), + confirm: async () => { + await Promise.all( + this._selected.map(async (applicationCredential) => { + await deleteApplicationCredential(this.hass, applicationCredential); + }) + ); + this._dataTable.clearSelection(); + this._fetchApplicationCredentials(); + }, + }); + } + + private async _loadTranslations() { + await this.hass.loadBackendTranslation("title", undefined, true); + } + + private async _fetchApplicationCredentials() { + this._applicationCredentials = await fetchApplicationCredentials(this.hass); + } + + private _addApplicationCredential() { + showAddApplicationCredentialDialog(this, { + applicationCredentialAddedCallback: async ( + applicationCredential: ApplicationCredential + ) => { + if (applicationCredential) { + this._applicationCredentials = [ + ...this._applicationCredentials, + applicationCredential, + ]; + } + }, + }); + } + + static get styles(): CSSResultGroup { + return css` + .table-header { + display: flex; + justify-content: space-between; + align-items: center; + height: 56px; + background-color: var(--mdc-text-field-fill-color, whitesmoke); + border-bottom: 1px solid + var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42)); + box-sizing: border-box; + } + .header-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--secondary-text-color); + position: relative; + top: -4px; + } + .selected-txt { + font-weight: bold; + padding-left: 16px; + } + .table-header .selected-txt { + margin-top: 20px; + } + .header-toolbar .selected-txt { + font-size: 16px; + } + .header-toolbar .header-btns { + margin-right: -12px; + } + .header-btns { + display: flex; + } + .header-btns > mwc-button, + .header-btns > ha-icon-button { + margin: 8px; + } + ha-button-menu { + margin-left: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-application-credentials": HaConfigApplicationCredentials; + } +} diff --git a/src/panels/config/application_credentials/show-dialog-add-application-credential.ts b/src/panels/config/application_credentials/show-dialog-add-application-credential.ts new file mode 100644 index 0000000000..1b779f60b3 --- /dev/null +++ b/src/panels/config/application_credentials/show-dialog-add-application-credential.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { ApplicationCredential } from "../../../data/application_credential"; + +export interface AddApplicationCredentialDialogParams { + applicationCredentialAddedCallback: ( + applicationCredential: ApplicationCredential + ) => void; +} + +export const loadAddApplicationCredentialDialog = () => + import("./dialog-add-application-credential"); + +export const showAddApplicationCredentialDialog = ( + element: HTMLElement, + dialogParams: AddApplicationCredentialDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-add-application-credential", + dialogImport: loadAddApplicationCredentialDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index d7027a9512..460356a2bb 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -16,9 +16,9 @@ import { } from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-button-menu"; +import "../../../components/ha-check-list-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; -import "../../../components/ha-check-list-item"; import { AreaRegistryEntry } from "../../../data/area_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { @@ -36,6 +36,7 @@ import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; +import "../integrations/ha-integration-overflow-menu"; import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; interface DeviceRowData extends DeviceRegistryEntry { @@ -408,6 +409,10 @@ export class HaConfigDeviceDashboard extends LitElement { (filteredConfigEntry.domain === "zha" || filteredConfigEntry.domain === "zwave_js")} > + ${!filteredConfigEntry ? "" : filteredConfigEntry.domain === "zwave_js" diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 6fd3314830..8385f08758 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -61,6 +61,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; +import "../integrations/ha-integration-overflow-menu"; import { DialogEntityEditor } from "./dialog-entity-editor"; import { loadEntityEditorDialog, @@ -526,6 +527,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { id="entity_id" .hasFab=${includeZHAFab} > + ${this._selectedEntities.length ? html`
+ import("./application_credentials/ha-config-application-credentials"), + }, }, }; diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index fbaa0e1c2f..228576574d 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -35,6 +35,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; +import "../integrations/ha-integration-overflow-menu"; import { HELPER_DOMAINS } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; @@ -210,6 +211,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { "ui.panel.config.helpers.picker.no_helpers" )} > + - ${!this._showDisabled && this.narrow && disabledCount - ? html`${disabledCount}` - : ""} - - - - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.show_ignored" - )} - - - ${this.hass.localize( - "ui.panel.config.integrations.disable.show_disabled" - )} - - -
`; + const filterMenu = html` +
+ + ${this.narrow + ? html` + + ` + : ""} +
+ `; return html`