mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-18 15:37:57 +00:00
Compare commits
1 Commits
clock-date
...
fix-issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
036ae921e7 |
@@ -572,7 +572,6 @@ export class StatisticsChart extends LitElement {
|
||||
let firstSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const startDate = new Date(stat.start);
|
||||
const endDate = new Date(stat.end);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
@@ -602,25 +601,10 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
// Close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
@@ -635,6 +619,16 @@ export class StatisticsChart extends LitElement {
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -23,37 +22,11 @@ export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
// @ts-ignore Allow to set an alternative anchor element
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
// @ts-ignore fallback to default trigger slot if no anchorElement is set
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -47,10 +47,6 @@ import type {
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type {
|
||||
HaDropdown,
|
||||
HaDropdownSelectEvent,
|
||||
} from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
@@ -61,6 +57,10 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -84,9 +84,9 @@ import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
deserializeFilters,
|
||||
serializeFilters,
|
||||
isFilterUsed,
|
||||
isRelatedItemsFilterUsed,
|
||||
serializeFilters,
|
||||
} from "../../../data/data_table_filters";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type {
|
||||
@@ -111,16 +111,16 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getEntityIdHiddenTableColumn,
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getEntityIdHiddenTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
@@ -129,6 +129,7 @@ import {
|
||||
} from "../voice-assistants/expose/assistants-table-column";
|
||||
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
@@ -222,7 +223,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
@@ -232,8 +233,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
return getAvailableAssistants(this.cloudStatus, this.hass);
|
||||
}
|
||||
|
||||
private _openingOverflow = false;
|
||||
|
||||
private _automations = memoizeOne(
|
||||
(
|
||||
automations: AutomationEntity[],
|
||||
@@ -372,27 +371,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (this._overflowMenu.anchorElement === ev.target) {
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._openingOverflow = true;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._overflowMenu.open = true;
|
||||
};
|
||||
|
||||
private _overflowMenuOpened = () => {
|
||||
this._openingOverflow = false;
|
||||
};
|
||||
|
||||
private _overflowMenuClosed = () => {
|
||||
// changing the anchorElement triggers a close event, ignore it
|
||||
if (this._openingOverflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -709,58 +697,74 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-dropdown
|
||||
id="overflow-menu"
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
<ha-dropdown-item value="show_info">
|
||||
<ha-svg-icon .path=${mdiInformationOutline} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</ha-dropdown-item>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._showInfo}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-dropdown-item value="show_settings">
|
||||
<ha-svg-icon .path=${mdiCog} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_category">
|
||||
<ha-svg-icon .path=${mdiTag} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="run_actions">
|
||||
<ha-svg-icon .path=${mdiPlay} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show_trace">
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_trace")}
|
||||
</ha-dropdown-item>
|
||||
<ha-md-menu-item .clickAction=${this._showSettings}>
|
||||
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._editCategory}>
|
||||
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runActions}>
|
||||
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._showTrace}>
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="toggle">
|
||||
<ha-md-menu-item .clickAction=${this._duplicate}>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggle}>
|
||||
<ha-svg-icon
|
||||
.path=${this._overflowAutomation?.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline}
|
||||
slot="icon"
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div slot="headline">
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -897,59 +901,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action || !this._overflowAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "show_info":
|
||||
this._showInfo(this._overflowAutomation);
|
||||
break;
|
||||
case "show_settings":
|
||||
this._showSettings(this._overflowAutomation);
|
||||
break;
|
||||
case "edit_category":
|
||||
this._editCategory(this._overflowAutomation);
|
||||
break;
|
||||
case "run_actions":
|
||||
this._runActions(this._overflowAutomation);
|
||||
break;
|
||||
case "show_trace":
|
||||
this._showTrace(this._overflowAutomation);
|
||||
break;
|
||||
case "toggle":
|
||||
this._toggle(this._overflowAutomation);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm(this._overflowAutomation);
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicate(this._overflowAutomation);
|
||||
break;
|
||||
}
|
||||
private _showInfo = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
|
||||
};
|
||||
|
||||
private _showInfo = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
});
|
||||
};
|
||||
private _showSettings = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _showSettings = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
};
|
||||
|
||||
private _runActions = (automation: AutomationItem) => {
|
||||
private _runActions = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
triggerAutomationActions(this.hass, automation.entity_id);
|
||||
};
|
||||
|
||||
private _editCategory = (automation: AutomationItem) => {
|
||||
private _editCategory = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
@@ -970,7 +948,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTrace = (automation: AutomationItem) => {
|
||||
private _showTrace = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
if (!automation.attributes.id) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -984,14 +965,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggle = async (automation: AutomationItem): Promise<void> => {
|
||||
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
const service = automation.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _deleteConfirm = async (automation: AutomationItem) => {
|
||||
private _deleteConfirm = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_title"
|
||||
@@ -1007,9 +994,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private async _delete(automation: AutomationItem) {
|
||||
private async _delete(automation) {
|
||||
try {
|
||||
await deleteAutomation(this.hass, automation.attributes.id!);
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
);
|
||||
@@ -1028,11 +1015,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicate = async (automation: AutomationItem) => {
|
||||
private _duplicate = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
try {
|
||||
const config = await fetchAutomationFileConfig(
|
||||
this.hass,
|
||||
automation.attributes.id!
|
||||
automation.attributes.id
|
||||
);
|
||||
duplicateAutomation(config);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -118,7 +118,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
path: "/config/matter",
|
||||
iconPath:
|
||||
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
|
||||
iconViewBox: "0 1 24 24",
|
||||
iconColor: "#2458B3",
|
||||
component: "matter",
|
||||
translationKey: "matter",
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ClockCardConfig } from "../types";
|
||||
@@ -28,11 +26,6 @@ function romanize12HourClock(num: number) {
|
||||
return numerals[num];
|
||||
}
|
||||
|
||||
const INTERVAL = 1000;
|
||||
const QUARTER_TICKS = Array.from({ length: 4 }, (_, i) => i);
|
||||
const HOUR_TICKS = Array.from({ length: 12 }, (_, i) => i);
|
||||
const MINUTE_TICKS = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
@customElement("hui-clock-card-analog")
|
||||
export class HuiClockCardAnalog extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -47,49 +40,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _secondOffsetSec?: number;
|
||||
|
||||
@state() private _year = "";
|
||||
|
||||
@state() private _month = "";
|
||||
|
||||
@state() private _day = "";
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _currentDate = new Date();
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeDateTime();
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._startTick();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeDateTime();
|
||||
}
|
||||
};
|
||||
|
||||
private _initDate() {
|
||||
if (!this.config || !this.hass) {
|
||||
return;
|
||||
@@ -101,27 +51,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -131,31 +60,42 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._computeDateTime();
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tick();
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateDate() {
|
||||
this._currentDate = new Date();
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
this._computeOffsets();
|
||||
}
|
||||
|
||||
private _computeDateTime() {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this._handleVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
this._computeOffsets();
|
||||
}
|
||||
};
|
||||
|
||||
private _computeOffsets() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
this._updateDate();
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts(this._currentDate);
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
const hourStr = parts.find((p) => p.type === "hour")?.value;
|
||||
const minuteStr = parts.find((p) => p.type === "minute")?.value;
|
||||
const secondStr = parts.find((p) => p.type === "second")?.value;
|
||||
@@ -163,7 +103,7 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
const hour = hourStr ? parseInt(hourStr, 10) : 0;
|
||||
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
|
||||
const second = secondStr ? parseInt(secondStr, 10) : 0;
|
||||
const ms = this._currentDate.getMilliseconds();
|
||||
const ms = new Date().getMilliseconds();
|
||||
const secondsWithMs = second + ms / 1000;
|
||||
|
||||
const hour12 = hour % 12;
|
||||
@@ -171,38 +111,18 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._secondOffsetSec = secondsWithMs;
|
||||
this._minuteOffsetSec = minute * 60 + secondsWithMs;
|
||||
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
|
||||
|
||||
// Also update date parts if date is shown
|
||||
if (this.config?.date && this.config.date !== "none") {
|
||||
this._year = parts.find((p) => p.type === "year")?.value ?? "";
|
||||
this._month = parts.find((p) => p.type === "month")?.value ?? "";
|
||||
this._day = parts.find((p) => p.type === "day")?.value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
this._computeDateTime();
|
||||
}
|
||||
|
||||
private _computeClock = memoizeOne((config: ClockCardConfig) => {
|
||||
const faceParts = config.face_style?.split("_");
|
||||
const isLongDate =
|
||||
config.date === "day-month-long" || config.date === "long";
|
||||
|
||||
return {
|
||||
sizeClass: config.clock_size ? `size-${config.clock_size}` : "",
|
||||
isNumbers: faceParts?.includes("numbers") ?? false,
|
||||
isRoman: faceParts?.includes("roman") ?? false,
|
||||
isUpright: faceParts?.includes("upright") ?? false,
|
||||
isLongDate,
|
||||
};
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.config) return nothing;
|
||||
|
||||
const { sizeClass, isNumbers, isRoman, isUpright, isLongDate } =
|
||||
this._computeClock(this.config);
|
||||
const sizeClass = this.config.clock_size
|
||||
? `size-${this.config.clock_size}`
|
||||
: "";
|
||||
|
||||
const isNumbers = this.config?.face_style?.startsWith("numbers");
|
||||
const isRoman = this.config?.face_style?.startsWith("roman");
|
||||
const isUpright = this.config?.face_style?.endsWith("upright");
|
||||
|
||||
const indicator = (number?: number) => html`
|
||||
<div
|
||||
@@ -243,14 +163,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
})}
|
||||
>
|
||||
${this.config.ticks === "quarter"
|
||||
? QUARTER_TICKS.map(
|
||||
? Array.from({ length: 4 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 4 ticks (12, 3, 6, 9) at 0°, 90°, 180°, 270°
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 90}deg` })}
|
||||
style=${`--tick-rotation: ${i * 90}deg;`}
|
||||
>
|
||||
${indicator([12, 3, 6, 9][i])}
|
||||
</div>
|
||||
@@ -258,30 +178,28 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
)
|
||||
: !this.config.ticks || // Default to hour ticks
|
||||
this.config.ticks === "hour"
|
||||
? HOUR_TICKS.map(
|
||||
? Array.from({ length: 12 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 12 ticks (1-12)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick hour"
|
||||
style=${styleMap({ "--tick-rotation": `${i * 30}deg` })}
|
||||
style=${`--tick-rotation: ${i * 30}deg;`}
|
||||
>
|
||||
${indicator(((i + 11) % 12) + 1)}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: this.config.ticks === "minute"
|
||||
? MINUTE_TICKS.map(
|
||||
? Array.from({ length: 60 }, (_, i) => i).map(
|
||||
(i) =>
|
||||
// 60 ticks (1-60)
|
||||
html`
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="tick ${i % 5 === 0 ? "hour" : "minute"}"
|
||||
style=${styleMap({
|
||||
"--tick-rotation": `${i * 6}deg`,
|
||||
})}
|
||||
style=${`--tick-rotation: ${i * 6}deg;`}
|
||||
>
|
||||
${i % 5 === 0
|
||||
? indicator(((i / 5 + 11) % 12) + 1)
|
||||
@@ -290,33 +208,14 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
`
|
||||
)
|
||||
: nothing}
|
||||
${this.config?.date && this.config.date !== "none"
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"date-parts": true,
|
||||
[sizeClass]: true,
|
||||
"long-date": isLongDate,
|
||||
})}
|
||||
>
|
||||
<span class="date-part day-month"
|
||||
>${this._day} ${this._month}</span
|
||||
>
|
||||
<span class="date-part year">${this._year}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="center-dot"></div>
|
||||
<div
|
||||
class="hand hour"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._hourOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
<div
|
||||
class="hand minute"
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${this._minuteOffsetSec ?? 0}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
|
||||
></div>
|
||||
${this.config.show_seconds
|
||||
? html`<div
|
||||
@@ -325,13 +224,11 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
second: true,
|
||||
step: this.config.seconds_motion === "tick",
|
||||
})}
|
||||
style=${styleMap({
|
||||
"animation-delay": `-${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s`,
|
||||
})}
|
||||
style=${`animation-delay: -${
|
||||
(this.config.seconds_motion === "tick"
|
||||
? Math.floor(this._secondOffsetSec ?? 0)
|
||||
: (this._secondOffsetSec ?? 0)) as number
|
||||
}s;`}
|
||||
></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -373,14 +270,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
.dial {
|
||||
container-type: inline-size;
|
||||
container-name: clock;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-border {
|
||||
border: 2px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
@@ -518,78 +407,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
transform: translate(-50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.date-parts {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
"day-month"
|
||||
"year";
|
||||
direction: ltr;
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
max-width: 87%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Modern browsers: Use container queries for responsive font sizing */
|
||||
@supports (container-type: inline-size) {
|
||||
/* Small clock with long date: reduce to xs */
|
||||
@container clock (max-width: 139px) {
|
||||
.date-parts.long-date {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium clock: scale up */
|
||||
@container clock (min-width: 140px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
/* Large clock: scale up more */
|
||||
@container clock (min-width: 200px) {
|
||||
.date-parts {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy browsers: Use existing size classes */
|
||||
@supports not (container-type: inline-size) {
|
||||
/* Small clock (no size class) with long date */
|
||||
.date-parts.long-date:not(.size-medium):not(.size-large) {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
.date-parts.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date-parts.size-large {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.date-part.day-month {
|
||||
grid-area: day-month;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.date-part.year {
|
||||
grid-area: year;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
private _initDate() {
|
||||
@@ -41,27 +39,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
|
||||
const h12 = useAmPm(locale);
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
...(this.config.date && this.config.date !== "none"
|
||||
? this.config.date === "day"
|
||||
? {
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month"
|
||||
? {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: this.config.date === "day-month-long"
|
||||
? {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}
|
||||
: {
|
||||
year: "numeric",
|
||||
month: this.config.date === "long" ? "long" : "short",
|
||||
day: "numeric",
|
||||
}
|
||||
: {}),
|
||||
hour: h12 ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -116,16 +93,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
|
||||
this._date = this.config?.date
|
||||
? [
|
||||
parts.find((part) => part.type === "day")?.value,
|
||||
parts.find((part) => part.type === "month")?.value,
|
||||
parts.find((part) => part.type === "year")?.value,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
: undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -146,9 +113,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.config.date && this.config.date !== "none"
|
||||
? html`<div class="date ${sizeClass}">${this._date}</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -224,20 +188,6 @@ export class HuiClockCardDigital extends LitElement {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.date {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
|
||||
.date.size-medium {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
.date.size-large {
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import {
|
||||
differenceInMonths,
|
||||
subHours,
|
||||
differenceInDays,
|
||||
differenceInMonths,
|
||||
differenceInCalendarMonths,
|
||||
differenceInYears,
|
||||
startOfYear,
|
||||
addMilliseconds,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
addHours,
|
||||
startOfDay,
|
||||
addDays,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
@@ -35,22 +33,10 @@ import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { StatisticPeriod } from "../../../../../data/recorder";
|
||||
import { getSuggestedPeriod } from "../../../../../data/energy";
|
||||
|
||||
// Number of days of padding when showing time axis in months
|
||||
const MONTH_TIME_AXIS_PADDING = 5;
|
||||
|
||||
export function getSuggestedMax(
|
||||
period: StatisticPeriod,
|
||||
end: Date,
|
||||
noRounding: boolean
|
||||
): Date {
|
||||
// Maximum period depends on whether plotting a line chart or discrete bars.
|
||||
// - For line charts we must be plotting all the way to end of a given period,
|
||||
// otherwise we cut off the last period of data.
|
||||
// - For bar charts we need to round down to the start of the final bars period
|
||||
// to avoid unnecessary padding of the chart.
|
||||
export function getSuggestedMax(period: StatisticPeriod, end: Date): Date {
|
||||
let suggestedMax = new Date(end);
|
||||
|
||||
if (noRounding || period === "5minute") {
|
||||
if (period === "5minute") {
|
||||
return suggestedMax;
|
||||
}
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
@@ -96,44 +82,17 @@ export function getCommonOptions(
|
||||
detailedDailyData = false
|
||||
): ECOption {
|
||||
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
|
||||
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
|
||||
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
const showCompareYear =
|
||||
compare && start.getFullYear() !== compareStart.getFullYear();
|
||||
|
||||
const monthTimeAxis: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: subDays(start, MONTH_TIME_AXIS_PADDING),
|
||||
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
|
||||
axisLabel: {
|
||||
formatter: {
|
||||
year: "{yearStyle|{MMMM} {yyyy}}",
|
||||
month: "{MMMM}",
|
||||
},
|
||||
rich: {
|
||||
yearStyle: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
// For shorter month ranges, force splitting to ensure time axis renders
|
||||
// as whole month intervals. Limit the number of forced ticks to 6 months
|
||||
// (so a max calendar difference of 5) to reduce clutter.
|
||||
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
|
||||
},
|
||||
};
|
||||
const normalTimeAxis: ECOption = {
|
||||
const options: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
max: suggestedMax,
|
||||
max: getSuggestedMax(suggestedPeriod, end),
|
||||
},
|
||||
};
|
||||
|
||||
const options: ECOption = {
|
||||
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: unit,
|
||||
|
||||
@@ -332,11 +332,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
.startTime=${this._energyStart}
|
||||
.endTime=${this._energyEnd && this._energyStart
|
||||
? getSuggestedMax(
|
||||
this._period!,
|
||||
this._energyEnd,
|
||||
(this._config.chart_type ?? "line") === "line"
|
||||
)
|
||||
? getSuggestedMax(this._period!, this._energyEnd)
|
||||
: undefined}
|
||||
.fitYData=${this._config.fit_y_data || false}
|
||||
.hideLegend=${this._config.hide_legend || false}
|
||||
|
||||
@@ -435,7 +435,6 @@ export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
time_format?: TimeFormat;
|
||||
time_zone?: string;
|
||||
no_background?: boolean;
|
||||
date?: "none" | "short" | "long" | "day" | "day-month" | "day-month-long";
|
||||
// Analog clock options
|
||||
border?: boolean;
|
||||
ticks?: "none" | "quarter" | "hour" | "minute";
|
||||
|
||||
@@ -39,19 +39,6 @@ const cardConfigStruct = assign(
|
||||
time_zone: optional(enums(Object.keys(timezones))),
|
||||
show_seconds: optional(boolean()),
|
||||
no_background: optional(boolean()),
|
||||
date: optional(
|
||||
defaulted(
|
||||
union([
|
||||
literal("none"),
|
||||
literal("short"),
|
||||
literal("long"),
|
||||
literal("day"),
|
||||
literal("day-month"),
|
||||
literal("day-month-long"),
|
||||
]),
|
||||
literal("none")
|
||||
)
|
||||
),
|
||||
// Analog clock options
|
||||
border: optional(defaulted(boolean(), false)),
|
||||
ticks: optional(
|
||||
@@ -106,7 +93,7 @@ export class HuiClockCardEditor
|
||||
name: "clock_style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
mode: "dropdown",
|
||||
options: ["digital", "analog"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
@@ -132,27 +119,6 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{ name: "show_seconds", selector: { boolean: {} } },
|
||||
{ name: "no_background", selector: { boolean: {} } },
|
||||
{
|
||||
name: "date",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: [
|
||||
"none",
|
||||
"short",
|
||||
"long",
|
||||
"day",
|
||||
"day-month",
|
||||
"day-month-long",
|
||||
].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.dates.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(clockStyle === "digital"
|
||||
? ([
|
||||
{
|
||||
@@ -275,13 +241,12 @@ export class HuiClockCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config: ClockCardConfig) => ({
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_style: "digital",
|
||||
clock_size: "small",
|
||||
time_format: "auto",
|
||||
show_seconds: false,
|
||||
no_background: false,
|
||||
date: "none",
|
||||
// Analog clock options
|
||||
border: false,
|
||||
ticks: "hour",
|
||||
@@ -305,9 +270,8 @@ export class HuiClockCardEditor
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(
|
||||
this.hass.localize,
|
||||
this._data(this._config)
|
||||
.clock_style as ClockCardConfig["clock_style"],
|
||||
this._data(this._config).ticks as ClockCardConfig["ticks"],
|
||||
this._data(this._config).clock_style,
|
||||
this._data(this._config).ticks,
|
||||
this._data(this._config).show_seconds
|
||||
)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -380,10 +344,6 @@ export class HuiClockCardEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.no_background`
|
||||
);
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.label`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.label`
|
||||
@@ -409,10 +369,6 @@ export class HuiClockCardEditor
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "date":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.description`
|
||||
);
|
||||
case "border":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import "../../components/ha-markdown";
|
||||
import "../../components/ha-spinner";
|
||||
import { autocompleteLoginFields } from "../../data/auth";
|
||||
@@ -31,7 +28,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _open = false;
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _stepData: any = {};
|
||||
|
||||
@@ -42,7 +39,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
||||
this._instance = instance++;
|
||||
this._dialogClosedCallback = dialogClosedCallback;
|
||||
this._open = true;
|
||||
this._opened = true;
|
||||
|
||||
const fetchStep = continueFlowId
|
||||
? this.hass.callWS({
|
||||
@@ -64,29 +61,22 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
// Closed dialog by clicking on the overlay
|
||||
if (this._step) {
|
||||
this._flowDone();
|
||||
return;
|
||||
}
|
||||
|
||||
this._resetDialogState();
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._instance === undefined) {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
header-title=${this._computeStepTitle()}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${this._computeStepTitle()}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div>
|
||||
${this._errorMessage
|
||||
@@ -125,7 +115,6 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
)}
|
||||
></ha-markdown>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.data=${this._stepData}
|
||||
.schema=${autocompleteLoginFields(
|
||||
@@ -138,33 +127,31 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
></ha-form>`
|
||||
: ""}`}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot=${this._step?.type === "form"
|
||||
? "secondaryAction"
|
||||
: "primaryAction"}
|
||||
appearance=${ifDefined(
|
||||
this._step?.type === "form" ? "plain" : undefined
|
||||
)}
|
||||
@click=${this.closeDialog}
|
||||
>${this.hass.localize(
|
||||
["abort", "create_entry"].includes(this._step?.type || "")
|
||||
? "ui.panel.profile.mfa_setup.close"
|
||||
: "ui.common.cancel"
|
||||
)}</ha-button
|
||||
>
|
||||
${this._step?.type === "form"
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._isSubmitDisabled()}
|
||||
@click=${this._submitStep}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.submit"
|
||||
)}</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
appearance=${["abort", "create_entry"].includes(
|
||||
this._step?.type || ""
|
||||
)
|
||||
? "accent"
|
||||
: "plain"}
|
||||
>${this.hass.localize(
|
||||
["abort", "create_entry"].includes(this._step?.type || "")
|
||||
? "ui.panel.profile.mfa_setup.close"
|
||||
: "ui.common.cancel"
|
||||
)}</ha-button
|
||||
>
|
||||
${this._step?.type === "form"
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._loading}
|
||||
@click=${this._submitStep}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.submit"
|
||||
)}</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -175,6 +162,9 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
ha-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-svg-background-color: white;
|
||||
--markdown-svg-color: black;
|
||||
@@ -187,17 +177,9 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
ha-markdown-element p {
|
||||
text-align: center;
|
||||
}
|
||||
ha-markdown-element svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-markdown-element code {
|
||||
background-color: transparent;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
ha-markdown-element > *:last-child {
|
||||
margin-bottom: revert;
|
||||
}
|
||||
@@ -224,10 +206,6 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _submitStep() {
|
||||
if (this._isSubmitDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
this._errorMessage = undefined;
|
||||
|
||||
@@ -256,62 +234,6 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _isSubmitDisabled() {
|
||||
return this._loading || this._hasMissingRequiredFields();
|
||||
}
|
||||
|
||||
private _hasMissingRequiredFields(
|
||||
schema: readonly HaFormSchema[] = this._step?.type === "form"
|
||||
? this._step.data_schema
|
||||
: []
|
||||
): boolean {
|
||||
for (const field of schema) {
|
||||
if ("schema" in field) {
|
||||
if (this._hasMissingRequiredFields(field.schema)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!field.required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
field.default !== undefined ||
|
||||
field.description?.suggested_value !== undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._isEmptyValue(this._stepData[field.name])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _isEmptyValue(value: unknown): boolean {
|
||||
if (value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return Object.keys(value as Record<string, unknown>).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _processStep(step) {
|
||||
if (!step.errors) step.errors = {};
|
||||
this._step = step;
|
||||
@@ -329,15 +251,12 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
this._dialogClosedCallback!({
|
||||
flowFinished,
|
||||
});
|
||||
this._resetDialogState();
|
||||
}
|
||||
|
||||
private _resetDialogState() {
|
||||
this._errorMessage = undefined;
|
||||
this._step = undefined;
|
||||
this._stepData = {};
|
||||
this._dialogClosedCallback = undefined;
|
||||
this._instance = undefined;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _computeStepTitle() {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { mdiContentCopy, mdiQrcode } from "@mdi/js";
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { withViewTransition } from "../../common/util/view-transition";
|
||||
import "../../components/ha-alert";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
||||
@@ -21,221 +20,81 @@ const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
|
||||
export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LongLivedAccessTokenDialogParams;
|
||||
|
||||
@state() private _qrCode?: TemplateResult;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _renderDialog = false;
|
||||
|
||||
@state() private _name = "";
|
||||
|
||||
@state() private _token?: string;
|
||||
|
||||
private _createdCallback!: () => void;
|
||||
|
||||
private _existingNames = new Set<string>();
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _errorMessage?: string;
|
||||
|
||||
public showDialog(params: LongLivedAccessTokenDialogParams): void {
|
||||
this._createdCallback = params.createdCallback;
|
||||
this._existingNames = new Set(
|
||||
params.existingNames.map((name) => this._normalizeName(name))
|
||||
);
|
||||
this._renderDialog = true;
|
||||
this._open = true;
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
this._renderDialog = false;
|
||||
this._name = "";
|
||||
this._token = undefined;
|
||||
this._existingNames = new Set();
|
||||
this._errorMessage = undefined;
|
||||
this._loading = false;
|
||||
this._params = undefined;
|
||||
this._qrCode = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._renderDialog) {
|
||||
if (!this._params || !this._params.token) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._token
|
||||
? this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.created_title",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
.heading=${createCloseHeading(this.hass, this._params.name)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div class="content">
|
||||
${this._errorMessage
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._errorMessage}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
${this._token
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
||||
)}
|
||||
</p>
|
||||
<div class="token-row">
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._token}
|
||||
type="text"
|
||||
readOnly
|
||||
></ha-textfield>
|
||||
<ha-button appearance="plain" @click=${this._copyToken}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.copy")}
|
||||
<div>
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._params.token}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
|
||||
)}
|
||||
type="text"
|
||||
iconTrailing
|
||||
readOnly
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._copyToken}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
<div id="qr">
|
||||
${this._qrCode
|
||||
? this._qrCode
|
||||
: html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._generateQR}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.generate_qr_code"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
<div id="qr">
|
||||
${this._qrCode
|
||||
? this._qrCode
|
||||
: html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._generateQR}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiQrcode}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.generate_qr_code"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name"
|
||||
)}
|
||||
.invalid=${this._hasDuplicateName()}
|
||||
.errorMessage=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name_exists"
|
||||
)}
|
||||
required
|
||||
@input=${this._nameChanged}
|
||||
></ha-textfield>
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
${this._token
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>`}
|
||||
${!this._token
|
||||
? html`<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._isCreateDisabled()}
|
||||
@click=${this._createToken}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create"
|
||||
)}
|
||||
</ha-button>`
|
||||
: html`<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>`}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _nameChanged(ev: Event) {
|
||||
this._name = (ev.currentTarget as HTMLInputElement).value;
|
||||
this._errorMessage = undefined;
|
||||
}
|
||||
|
||||
private _isCreateDisabled() {
|
||||
return this._loading || !this._name.trim() || this._hasDuplicateName();
|
||||
}
|
||||
|
||||
private async _createToken(): Promise<void> {
|
||||
if (this._isCreateDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = this._name.trim();
|
||||
|
||||
this._loading = true;
|
||||
this._errorMessage = undefined;
|
||||
|
||||
try {
|
||||
this._token = await this.hass.callWS<string>({
|
||||
type: "auth/long_lived_access_token",
|
||||
lifespan: 3650,
|
||||
client_name: name,
|
||||
});
|
||||
this._name = name;
|
||||
this._createdCallback();
|
||||
} catch (err: unknown) {
|
||||
this._errorMessage = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyToken(): Promise<void> {
|
||||
if (!this._token) {
|
||||
return;
|
||||
}
|
||||
|
||||
await copyToClipboard(this._token);
|
||||
private async _copyToken(ev): Promise<void> {
|
||||
const textField = ev.target.parentElement as HaTextField;
|
||||
await copyToClipboard(textField.value);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _normalizeName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
private _hasDuplicateName(): boolean {
|
||||
return this._existingNames.has(this._normalizeName(this._name));
|
||||
}
|
||||
|
||||
private async _generateQR() {
|
||||
if (!this._token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qrcode = await import("qrcode");
|
||||
const canvas = await qrcode.toCanvas(this._token, {
|
||||
width: 512,
|
||||
const canvas = await qrcode.toCanvas(this._params!.token, {
|
||||
width: 180,
|
||||
errorCorrectionLevel: "Q",
|
||||
});
|
||||
const context = canvas.getContext("2d");
|
||||
@@ -253,46 +112,35 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
|
||||
canvas.height / 3
|
||||
);
|
||||
|
||||
await withViewTransition(() => {
|
||||
this._qrCode = html`<img
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.qr_code_image",
|
||||
{ name: this._name }
|
||||
)}
|
||||
src=${canvas.toDataURL()}
|
||||
></img>`;
|
||||
});
|
||||
this._qrCode = html`<img
|
||||
alt=${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.qr_code_image",
|
||||
{ name: this._params!.name }
|
||||
)}
|
||||
src=${canvas.toDataURL()}
|
||||
></img>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
#qr {
|
||||
text-align: center;
|
||||
}
|
||||
#qr img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.token-row {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.token-row ha-textfield {
|
||||
flex: 1;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
--textfield-icon-trailing-padding: 0;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: relative;
|
||||
right: -8px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: -8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { RefreshToken } from "../../data/refresh_token";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -25,14 +26,14 @@ class HaLongLivedTokens extends LitElement {
|
||||
@property({ attribute: false }) public refreshTokens?: RefreshToken[];
|
||||
|
||||
private _accessTokens = memoizeOne(
|
||||
(refreshTokens?: RefreshToken[]): RefreshToken[] =>
|
||||
(refreshTokens ?? [])
|
||||
.filter((token) => token.type === "long_lived_access_token")
|
||||
(refreshTokens: RefreshToken[]): RefreshToken[] =>
|
||||
refreshTokens
|
||||
?.filter((token) => token.type === "long_lived_access_token")
|
||||
.reverse()
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const accessTokens = this._accessTokens(this.refreshTokens);
|
||||
const accessTokens = this._accessTokens(this.refreshTokens!);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@@ -54,13 +55,13 @@ class HaLongLivedTokens extends LitElement {
|
||||
"ui.panel.profile.long_lived_access_tokens.learn_auth_requests"
|
||||
)}
|
||||
</a>
|
||||
${!accessTokens.length
|
||||
${!accessTokens?.length
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.empty_state"
|
||||
)}
|
||||
</p>`
|
||||
: accessTokens.map(
|
||||
: accessTokens!.map(
|
||||
(token) =>
|
||||
html`<ha-settings-row two-line>
|
||||
<span slot="heading">${token.client_name}</span>
|
||||
@@ -97,15 +98,38 @@ class HaLongLivedTokens extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _createToken(): void {
|
||||
const accessTokens = this._accessTokens(this.refreshTokens);
|
||||
|
||||
showLongLivedAccessTokenDialog(this, {
|
||||
createdCallback: () => fireEvent(this, "hass-refresh-tokens"),
|
||||
existingNames: accessTokens
|
||||
.map((token) => token.client_name)
|
||||
.filter((name): name is string => Boolean(name)),
|
||||
private async _createToken(): Promise<void> {
|
||||
const name = await showPromptDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.prompt_name"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.name"
|
||||
),
|
||||
});
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await this.hass.callWS<string>({
|
||||
type: "auth/long_lived_access_token",
|
||||
lifespan: 3650,
|
||||
client_name: name,
|
||||
});
|
||||
|
||||
showLongLivedAccessTokenDialog(this, { token, name });
|
||||
|
||||
fireEvent(this, "hass-refresh-tokens");
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.long_lived_access_tokens.create_failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteToken(ev: Event): Promise<void> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface LongLivedAccessTokenDialogParams {
|
||||
createdCallback: () => void;
|
||||
existingNames: string[];
|
||||
token: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const showLongLivedAccessTokenDialog = (
|
||||
|
||||
@@ -8907,18 +8907,6 @@
|
||||
"large": "Large"
|
||||
},
|
||||
"show_seconds": "Display seconds",
|
||||
"date": {
|
||||
"label": "Date",
|
||||
"description": "Whether to show the date on the clock. Can also be a custom date format."
|
||||
},
|
||||
"dates": {
|
||||
"none": "No date",
|
||||
"short": "Short",
|
||||
"long": "Long",
|
||||
"day": "Day only",
|
||||
"day-month": "Day and month",
|
||||
"day-month-long": "Day and month (long)"
|
||||
},
|
||||
"time_format": "[%key:ui::panel::profile::time_format::dropdown_label%]",
|
||||
"time_formats": {
|
||||
"auto": "Use user settings",
|
||||
@@ -9739,10 +9727,8 @@
|
||||
"confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?",
|
||||
"delete_failed": "Failed to delete the access token.",
|
||||
"create": "Create token",
|
||||
"created_title": "Token created: {name}",
|
||||
"create_failed": "Failed to create the access token.",
|
||||
"name": "Name",
|
||||
"name_exists": "A token with this name already exists.",
|
||||
"prompt_name": "Give the token a name",
|
||||
"prompt_copy_token": "Copy your access token. It will not be shown again.",
|
||||
"empty_state": "You have no long-lived access tokens yet.",
|
||||
|
||||
Reference in New Issue
Block a user