mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-30 17:43:39 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c350d46d44 | ||
|
|
e5a43b48c2 | ||
|
|
5f11dc4580 | ||
|
|
27489f188e | ||
|
|
97fffc3b43 | ||
|
|
a30193b8ef | ||
|
|
cf063dc19f | ||
|
|
5db829b7be | ||
|
|
421f8b1986 | ||
|
|
a140eedc6e | ||
|
|
2933568658 | ||
|
|
29faeaf7fa | ||
|
|
95ba532ceb | ||
|
|
352ecb9d6d | ||
|
|
3f73049a3d | ||
|
|
50ad0c6cd0 | ||
|
|
78f95bbb06 |
No files matched your search
@@ -51,8 +51,8 @@
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to [documentation][docs-repository] pull request:
|
||||
- Link to [contributors documentation][contributors-repository] pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
@@ -63,7 +63,8 @@
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
understanding the code before submitting a PR. Please follow our AI policy:
|
||||
https://contributors.home-assistant.io/start/ai-policy/
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
@@ -105,4 +106,5 @@ To help with the load of incoming pull requests:
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[contributors-repository]: https://github.com/home-assistant/contributors.home-assistant.io
|
||||
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import type { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
@@ -215,7 +214,7 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
private async _handlePickView(ev: CustomEvent<{ index: number }>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.8.0",
|
||||
"home-assistant-js-websocket": "9.8.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"marked": "18.1.1",
|
||||
"marked": "18.1.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
|
||||
Generated
+10
-10
@@ -364,8 +364,8 @@ importers:
|
||||
specifier: 1.7.3
|
||||
version: 1.7.3
|
||||
home-assistant-js-websocket:
|
||||
specifier: 9.8.0
|
||||
version: 9.8.0
|
||||
specifier: 9.8.1
|
||||
version: 9.8.1
|
||||
idb-keyval:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
@@ -391,8 +391,8 @@ importers:
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
marked:
|
||||
specifier: 18.1.1
|
||||
version: 18.1.1
|
||||
specifier: 18.1.2
|
||||
version: 18.1.2
|
||||
memoize-one:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -5205,8 +5205,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==}
|
||||
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-Nl5asoKQJwJ5a8N0U72jkSAUdO+hsY/T+Upp0VaZMUSt6A2gtdyvKfBIEYbpnYK3NP83Q/YGaC5XYkO5c08Img==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-WvovbrSoYLTKfh6aY5SxSWPPy50/mLWLCihQL8G2N+2e3mTrg3QTQCAp4R32/LwbkRBDGj0XNs6JFj8e2IYEUw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
|
||||
@@ -5907,8 +5907,8 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==}
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-DfOnvLAQ6WYeWxWO1u4RyFOeqyHup6dks5PPqnoFiooU7ipRvidO/2j8plhAHIiZg574E9ZlzSjUiPMLAzFkgg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
@@ -12962,7 +12962,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].0: {}
|
||||
[email protected].1: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13689,7 +13689,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].1: {}
|
||||
[email protected].2: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
|
||||
@@ -32,16 +32,14 @@ export class HaPickAuthProvider extends LitElement {
|
||||
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<ha-list-base
|
||||
aria-label=${this._localize("ui.panel.page-authorize.pick_auth_provider")}
|
||||
>
|
||||
<ha-list-base>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item-button
|
||||
.headline=${provider.name}
|
||||
.auth_provider=${provider}
|
||||
@click=${this._handlePick}
|
||||
>
|
||||
<span slot="headline">${provider.name}</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
@@ -82,13 +80,7 @@ export class HaPickAuthProvider extends LitElement {
|
||||
}
|
||||
ha-list-base {
|
||||
margin: 16px -16px 0;
|
||||
--ha-list-padding: var(--ha-space-2) 0;
|
||||
}
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
--ha-row-item-padding-inline: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const RASTER_TILE_URL = `${MAP_TILES_PATH}/raster/{z}/{x}/{y}.png?token={token}`
|
||||
// policy strips again unless the tiles ask for it back.
|
||||
const DEMO_RASTER_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const OSM_ATTRIBUTION =
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
'<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
||||
|
||||
// Browsers keep about 16 live WebGL contexts and drop the oldest, which a
|
||||
// dashboard full of map cards hits. A transient loss is restored, hence a grace.
|
||||
|
||||
@@ -89,6 +89,9 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
const CLICK_DRAG_TOLERANCE = 4;
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
@@ -163,15 +166,20 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "small-controls", type: Boolean })
|
||||
public smallControls?: boolean;
|
||||
|
||||
// For charts zoomed from outside, like the synced History charts
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
// "auto" shows the zoom slider once zoomed. "never" hides every zoom
|
||||
// control, for charts zoomed from outside like the synced History charts.
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
// For charts sized to their content, like timelines, which grow to fit the
|
||||
// zoom slider instead of giving up plot height
|
||||
@property({ attribute: "grow-for-zoom-slider", type: Boolean })
|
||||
public growForZoomSlider = false;
|
||||
|
||||
// Dragging selects a time range instead of zooming
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
// extraComponents is not reactive and should not trigger updates
|
||||
public extraComponents?: any[];
|
||||
|
||||
@@ -197,6 +205,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
private _pointerDownX?: number;
|
||||
|
||||
// Separate from _isZoomed, whose touch handling reruns graph layouts
|
||||
@state() private _isGraphRoamed = false;
|
||||
|
||||
@@ -450,6 +460,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("selectRange")) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
const themeChanged = changedProps.has("_themes") && this.hasUpdated;
|
||||
if (!themeChanged && !DEFERRED_PROPS.some((p) => changedProps.has(p))) {
|
||||
return;
|
||||
@@ -617,7 +630,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
>
|
||||
${
|
||||
(this._isZoomed || this._isGraphRoamed) &&
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
!showZoomSlider
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -901,6 +914,32 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this.chart.on("click", (e: ECElementEvent) => {
|
||||
fireEvent(this, "chart-click", e);
|
||||
});
|
||||
this.chart.getZr().on("mousedown", (e: ECElementEvent) => {
|
||||
this._pointerDownX = e.offsetX;
|
||||
});
|
||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||
const point = [e.offsetX, e.offsetY];
|
||||
// A drag that ends on the plot is not a click
|
||||
const dragged =
|
||||
this._pointerDownX !== undefined &&
|
||||
Math.abs(e.offsetX - this._pointerDownX) > CLICK_DRAG_TOLERANCE;
|
||||
if (dragged || !this.chart?.containPixel("grid", point)) {
|
||||
return;
|
||||
}
|
||||
const [x, y] = this.chart.convertFromPixel("grid", point) as number[];
|
||||
fireEvent(this, "chart-grid-click", { x, y });
|
||||
});
|
||||
this.chart.on("brushEnd", (e: any) => {
|
||||
const range = e.areas?.[0]?.coordRange as number[] | undefined;
|
||||
// The caller shows the selection, snapped to its data
|
||||
this.chart?.dispatchAction({ type: "brush", areas: [] });
|
||||
if (range) {
|
||||
fireEvent(this, "chart-range-select", {
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
});
|
||||
}
|
||||
});
|
||||
this.chart.on("sankeyroam", () => {
|
||||
const option = this.chart!.getOption();
|
||||
const series = option.series as any[];
|
||||
@@ -966,6 +1005,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this._legendItemCount = this._getLegendItems()?.length;
|
||||
this._updateSankeyRoam();
|
||||
this._updateDragToZoom();
|
||||
if (this.selectRange) {
|
||||
this._updateSelectRange();
|
||||
}
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
@@ -1017,7 +1059,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
// A chart can switch type (e.g. pie and bar) without being rebuilt
|
||||
private _updateDragToZoom() {
|
||||
const active = !this._touchInput && this._hasZoomableXAxis();
|
||||
const active =
|
||||
!this._touchInput && this._hasZoomableXAxis() && !this.selectRange;
|
||||
if (!this.chart || active === this._dragToZoom) {
|
||||
return;
|
||||
}
|
||||
@@ -1029,6 +1072,33 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _updateSelectRange() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (this.selectRange) {
|
||||
this.chart.setOption({
|
||||
brush: { xAxisIndex: 0, transformable: false },
|
||||
});
|
||||
}
|
||||
const brushCursor = {
|
||||
type: "takeGlobalCursor",
|
||||
key: "brush",
|
||||
brushOption: {
|
||||
brushType: this.selectRange ? "lineX" : false,
|
||||
brushMode: "single",
|
||||
},
|
||||
};
|
||||
// Brush and drag to zoom share the global cursor, the last one taken wins
|
||||
if (this.selectRange) {
|
||||
this._updateDragToZoom();
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
} else {
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
this._updateDragToZoom();
|
||||
}
|
||||
}
|
||||
|
||||
private _hasZoomableXAxis() {
|
||||
const xAxis = ensureArray(this.options?.xAxis)?.[0] as
|
||||
XAXisOption | undefined;
|
||||
@@ -1064,9 +1134,12 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _showZoomSlider() {
|
||||
const [start, end] = this._zoomRange;
|
||||
return (
|
||||
!this.hideZoomControls &&
|
||||
this.zoomSlider !== "never" &&
|
||||
this._hasZoomableXAxis() &&
|
||||
(this._zoomSliderHeld || start !== 0 || end !== 100)
|
||||
(this.zoomSlider === "always" ||
|
||||
this._zoomSliderHeld ||
|
||||
start !== 0 ||
|
||||
end !== 100)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1532,7 +1605,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleClickZoom = (e: ECElementEvent) => {
|
||||
if (!this.chart) {
|
||||
if (!this.chart || this.selectRange) {
|
||||
return;
|
||||
}
|
||||
// Handle sankey chart double-click zoom
|
||||
@@ -1745,7 +1818,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
// it, so a quick drag out would lose the release and its selection
|
||||
if (
|
||||
ev.type === "pointerdown" &&
|
||||
this._dragToZoom &&
|
||||
(this._dragToZoom || this.selectRange) &&
|
||||
this._isZoomPress(ev)
|
||||
) {
|
||||
this.chart
|
||||
@@ -2317,6 +2390,8 @@ declare global {
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
"chart-grid-click": { x: number; y: number };
|
||||
"chart-range-select": { start: number; end: number };
|
||||
"legend-label-click": { id: string };
|
||||
"chart-zoom": {
|
||||
start: number;
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import {
|
||||
@@ -105,8 +106,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@@ -159,7 +160,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { DEFAULT_CHART_WIDTH, MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -68,8 +69,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@@ -105,7 +106,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
grow-for-zoom-slider
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -49,7 +49,11 @@ import type {
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import type {
|
||||
CustomLegendOption,
|
||||
HaChartBase,
|
||||
ZoomSliderMode,
|
||||
} from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
@@ -128,6 +132,19 @@ export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "select-range", type: Boolean })
|
||||
public selectRange = false;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@property({ attribute: false }) public highlight?: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
@query("ha-chart-base") private _chartBase?: HaChartBase;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: CustomLegendOption["data"];
|
||||
@@ -243,9 +260,15 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.data=${this._highlightedChartData(
|
||||
this._chartData,
|
||||
this.highlight,
|
||||
getComputedStyle(this).getPropertyValue("--primary-color").trim()
|
||||
)}
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
.selectRange=${this.selectRange}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@@ -259,6 +282,59 @@ export class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public resetZoom() {
|
||||
this._chartBase?.zoom(0, 100);
|
||||
}
|
||||
|
||||
private _highlightedChartData = memoizeOne(
|
||||
(
|
||||
data: (LineSeriesOption | BarSeriesOption)[],
|
||||
highlight: { start: number; end: number } | undefined,
|
||||
color: string
|
||||
): (LineSeriesOption | BarSeriesOption)[] => {
|
||||
if (!data.length) {
|
||||
return data;
|
||||
}
|
||||
if (data[0].type === "bar") {
|
||||
if (!highlight) {
|
||||
return data;
|
||||
}
|
||||
// Bars outside the highlight fade, so the highlighted ones stand out.
|
||||
// Some points are already objects, like negative bars with round caps.
|
||||
return data.map((series) => ({
|
||||
...series,
|
||||
data: (series.data as any[]).map((item) => {
|
||||
const point =
|
||||
item && typeof item === "object" && "value" in item
|
||||
? item
|
||||
: { value: item };
|
||||
// Current state bars only carry their time
|
||||
const start = new Date(point.value[2] ?? point.value[0]).getTime();
|
||||
return start >= highlight.start && start < highlight.end
|
||||
? item
|
||||
: {
|
||||
...point,
|
||||
itemStyle: { ...point.itemStyle, opacity: 0.3 },
|
||||
};
|
||||
}),
|
||||
})) as BarSeriesOption[];
|
||||
}
|
||||
// Every series gets an area, empty when not highlighted, as the chart
|
||||
// merges series and would keep an old one
|
||||
return data.map((series, index) => ({
|
||||
...series,
|
||||
markArea: {
|
||||
silent: true,
|
||||
itemStyle: { color, opacity: 0.15 },
|
||||
data:
|
||||
highlight && index === 0
|
||||
? [[{ xAxis: highlight.start }, { xAxis: highlight.end }]]
|
||||
: [],
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
@@ -32,8 +32,8 @@ import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
import "../ha-icon-button-prev";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../item/ha-list-item-button";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
@@ -166,11 +166,19 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
<ha-list-base>
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-list-item-button
|
||||
.range=${range}
|
||||
.index=${index}
|
||||
@click=${this._setDateRange}
|
||||
>
|
||||
<span slot="headline">${name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -343,11 +351,12 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
private _setDateRange(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { range: [Date, Date]; index: number }
|
||||
>
|
||||
) {
|
||||
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { css } from "lit";
|
||||
import { RadioListItemBase } from "@material/mwc-list/mwc-radio-list-item-base";
|
||||
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-radio-list-item")
|
||||
export class HaRadioListItem extends RadioListItemBase {
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([graphic="avatar"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="medium"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="large"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="control"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 16px);
|
||||
margin-inline-start: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
flex-shrink: 0;
|
||||
direction: var(--direction);
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-top: var(--radio-list-item-graphic-margin-top);
|
||||
}
|
||||
:host([graphic="icon"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 32px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-list-item": HaRadioListItem;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import "../../components/ha-list-item";
|
||||
import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
@@ -80,27 +79,28 @@ class StepFlowMenu extends LitElement {
|
||||
|
||||
return html`
|
||||
${description ? html`<div class="content">${description}</div>` : nothing}
|
||||
<ha-list-base class="options">
|
||||
<div class="options">
|
||||
${options.map((option) => {
|
||||
const optionDescription = optionDescriptions[option];
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${translations[option]}
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.step=${option}
|
||||
@click=${this._handleStep}
|
||||
?twoline=${optionDescription}
|
||||
?multiline-secondary=${optionDescription}
|
||||
>
|
||||
<span>${translations[option]}</span>
|
||||
${
|
||||
optionDescription
|
||||
? html`<span slot="supporting-text" class="description"
|
||||
>${optionDescription}</span
|
||||
>`
|
||||
? html`<span slot="secondary"> ${optionDescription} </span>`
|
||||
: nothing
|
||||
}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -129,14 +129,8 @@ class StepFlowMenu extends LitElement {
|
||||
.content + .options {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
.description {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -18,10 +18,8 @@ import {
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import "../components/item/ha-list-item-base";
|
||||
import "../components/item/ha-list-item-option";
|
||||
import "../components/list/ha-list-base";
|
||||
import "../components/list/ha-list-selectable";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/ha-spinner";
|
||||
import "../components/input/ha-input";
|
||||
import type { HaInput } from "../components/input/ha-input";
|
||||
@@ -143,12 +141,10 @@ class OnboardingLocation extends LitElement {
|
||||
${
|
||||
this._places !== undefined
|
||||
? html`
|
||||
${
|
||||
this._places?.length
|
||||
? html`<ha-list-selectable
|
||||
@ha-list-item-selected=${this._placeSelected}
|
||||
>
|
||||
${this._places.map((place) => {
|
||||
<ha-list activatable>
|
||||
${
|
||||
this._places?.length
|
||||
? this._places.map((place) => {
|
||||
const primary = [
|
||||
place.name || place.address[place.category],
|
||||
place.address.house_number,
|
||||
@@ -168,19 +164,26 @@ class OnboardingLocation extends LitElement {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
return html`<ha-list-item-option
|
||||
return html`<ha-list-item
|
||||
@click=${this._itemClicked}
|
||||
.placeId=${place.place_id}
|
||||
.selected=${this._highlightedMarker === place.place_id}
|
||||
.headline=${primary || secondary}
|
||||
.supportingText=${primary ? secondary : undefined}
|
||||
></ha-list-item-option>`;
|
||||
})}
|
||||
</ha-list-selectable>`
|
||||
: html`<ha-list-base>
|
||||
<ha-list-item-base
|
||||
.headline=${this._places === null ? "" : "No results"}
|
||||
></ha-list-item-base>
|
||||
</ha-list-base>`
|
||||
}
|
||||
.activated=${this._highlightedMarker === place.place_id}
|
||||
.twoline=${primary && secondary}
|
||||
>
|
||||
${primary || secondary}
|
||||
<span slot="secondary"
|
||||
>${primary ? secondary : ""}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
})
|
||||
: html`<ha-list-item noninteractive
|
||||
>${
|
||||
this._places === null ? "" : "No results"
|
||||
}</ha-list-item
|
||||
>`
|
||||
}
|
||||
</ha-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -306,9 +309,11 @@ class OnboardingLocation extends LitElement {
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
|
||||
private _placeSelected(ev: CustomEvent<number>) {
|
||||
const place = this._places![ev.detail];
|
||||
this._highlightedMarker = place.place_id;
|
||||
private _itemClicked(ev) {
|
||||
this._highlightedMarker = ev.currentTarget.placeId;
|
||||
const place = this._places!.find(
|
||||
(plc) => plc.place_id === ev.currentTarget.placeId
|
||||
)!;
|
||||
this._location = [Number(place.lat), Number(place.lon)];
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
@@ -500,19 +505,17 @@ class OnboardingLocation extends LitElement {
|
||||
border-radius: var(--mdc-shape-large, 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-list-selectable,
|
||||
ha-list-base {
|
||||
display: block;
|
||||
ha-list {
|
||||
width: 100%;
|
||||
border: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
border-top-width: 0;
|
||||
border-bottom-left-radius: var(--mdc-shape-small, 4px);
|
||||
border-bottom-right-radius: var(--mdc-shape-small, 4px);
|
||||
--mdc-list-vertical-padding: 0;
|
||||
}
|
||||
ha-list-item-option,
|
||||
ha-list-item-base {
|
||||
--ha-row-item-min-height: 72px;
|
||||
ha-list-item {
|
||||
height: 72px;
|
||||
}
|
||||
.attribution {
|
||||
/* input helper style */
|
||||
|
||||
@@ -7,11 +7,11 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/input/ha-input";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import type { InputSelect } from "../../../../data/input_select";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
@@ -108,26 +108,24 @@ class HaInputSelectForm extends LitElement {
|
||||
handle-selector=".handle"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-base
|
||||
class="options"
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.input_select.options"
|
||||
)}
|
||||
>
|
||||
<ha-list class="options">
|
||||
${
|
||||
this._options.length
|
||||
? repeat(
|
||||
this._options,
|
||||
(option) => option,
|
||||
(option, index) => html`
|
||||
<ha-list-item-base class="option" .headline=${option}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-list-item class="option" hasMeta>
|
||||
<div class="optioncontent">
|
||||
<div class="handle">
|
||||
<ha-svg-icon
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
${option}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
slot="meta"
|
||||
.index=${index}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.input_select.remove_option"
|
||||
@@ -136,12 +134,12 @@ class HaInputSelectForm extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</ha-list-item-base>
|
||||
</ha-list-item>
|
||||
`
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
</ha-sortable>
|
||||
<div class="layout horizontal center">
|
||||
<ha-input
|
||||
@@ -238,18 +236,13 @@ class HaInputSelectForm extends LitElement {
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.options {
|
||||
--ha-list-padding: var(--ha-space-2) 0;
|
||||
}
|
||||
.option ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.option {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin-top: var(--ha-space-1);
|
||||
--ha-icon-button-size: 24px;
|
||||
--ha-row-item-gap: var(--ha-space-3);
|
||||
--mdc-ripple-color: transparent;
|
||||
--mdc-list-side-padding: var(--ha-space-4);
|
||||
cursor: default;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
@@ -265,6 +258,17 @@ class HaInputSelectForm extends LitElement {
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding-right: var(--ha-space-3);
|
||||
padding-inline-end: var(--ha-space-3);
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
height: 24px;
|
||||
}
|
||||
.optioncontent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-icon-picker {
|
||||
display: block;
|
||||
|
||||
@@ -12,9 +12,9 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
getCoreLogFileDownloadUnavailableReason,
|
||||
@@ -227,18 +227,16 @@ export class SystemLogCard extends LitElement {
|
||||
)
|
||||
}
|
||||
</div>`
|
||||
: html`<ha-list-base
|
||||
: html`<ha-list
|
||||
>${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item-button
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
.headline=${item.message[0]}
|
||||
twoline
|
||||
>
|
||||
<span
|
||||
slot="supporting-text"
|
||||
class="secondary"
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
@@ -267,9 +265,9 @@ export class SystemLogCard extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list-base
|
||||
)}</ha-list
|
||||
>`
|
||||
}
|
||||
|
||||
@@ -340,9 +338,8 @@ export class SystemLogCard extends LitElement {
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-list-base {
|
||||
ha-list {
|
||||
direction: ltr;
|
||||
--ha-list-padding: var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -14,14 +14,15 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../../components/radio/ha-radio-group";
|
||||
import "../../../components/radio/ha-radio-option";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-tab-group";
|
||||
import "../../../components/ha-tab-group-tab";
|
||||
import "../../../components/input/ha-input";
|
||||
import "../../../components/item/ha-list-item-option";
|
||||
import "../../../components/list/ha-list-selectable";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
@@ -168,28 +169,29 @@ export class HassioNetwork extends LitElement {
|
||||
${
|
||||
this._accessPoints.length
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
@ha-list-item-selected=${this._selectAP}
|
||||
>
|
||||
<ha-list>
|
||||
${this._accessPoints.map(
|
||||
(ap) => html`
|
||||
<ha-list-item-option
|
||||
.headline=${ap.ssid}
|
||||
.selected=${
|
||||
<ha-list-item
|
||||
twoline
|
||||
@click=${this._selectAP}
|
||||
.activated=${
|
||||
ap.ssid === this._wifiConfiguration?.ssid
|
||||
}
|
||||
.ap=${ap}
|
||||
>
|
||||
<span slot="supporting-text">
|
||||
<span>${ap.ssid}</span>
|
||||
<span slot="secondary">
|
||||
${ap.mac} -
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.supervisor.signal_strength"
|
||||
)}:
|
||||
${ap.signal}
|
||||
</span>
|
||||
</ha-list-item-option>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
</ha-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
@@ -272,14 +274,8 @@ export class HassioNetwork extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _selectAP(ev: CustomEvent<number>) {
|
||||
const ap = this._accessPoints[ev.detail];
|
||||
this._wifiConfiguration = {
|
||||
mode: ap.mode,
|
||||
ssid: ap.ssid,
|
||||
signal: ap.signal,
|
||||
auth: "open",
|
||||
};
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
if (this._interface![version]!.method === "disabled") {
|
||||
this._interface![version]!.method = "auto";
|
||||
@@ -848,8 +844,8 @@ export class HassioNetwork extends LitElement {
|
||||
.add-nameserver {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-list-item-option {
|
||||
--ha-row-item-padding-inline: 10px;
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 10px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -5,9 +5,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/user/ha-person-badge";
|
||||
import type { Person } from "../../../data/person";
|
||||
import {
|
||||
@@ -92,23 +93,22 @@ export class HaConfigPerson extends LitElement {
|
||||
</span>
|
||||
|
||||
<ha-card outlined class="storage">
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this._storageItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
<ha-list-item-button
|
||||
@click=${this._openEditEntry}
|
||||
.entry=${entry}
|
||||
>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></ha-person-badge>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
${
|
||||
this._storageItems.length === 0
|
||||
? html`
|
||||
@@ -138,19 +138,19 @@ export class HaConfigPerson extends LitElement {
|
||||
this._configItems.length > 0
|
||||
? html`
|
||||
<ha-card outlined header="Configuration.yaml persons">
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${this._configItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item graphic="avatar">
|
||||
<ha-list-item-base>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></ha-person-badge>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing
|
||||
@@ -299,7 +299,7 @@ export class HaConfigPerson extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
ha-list:has(+ .empty) {
|
||||
ha-list-base:has(+ .empty) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -73,6 +73,10 @@ const mountPartitionLabel = ({
|
||||
return drive ? `${drive} — ${identity}, ${size}` : `${identity}, ${size}`;
|
||||
};
|
||||
|
||||
// Supervisor accepts only ASCII letters, digits and underscores.
|
||||
const mountNameFromLabel = (label?: string): string =>
|
||||
(label ?? "").replace(/^\W+|\W+$/g, "").replace(/\W+/g, "_");
|
||||
|
||||
const requiredFieldsFilled = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data?: MountFormData
|
||||
@@ -305,6 +309,8 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
|
||||
private _originalType?: SupervisorMountType;
|
||||
|
||||
private _suggestedName?: string;
|
||||
|
||||
@state() private _diskIdentity?: string;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -529,7 +535,9 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._validationError = {};
|
||||
this._validationWarning = {};
|
||||
const previous = this._data;
|
||||
this._data = ev.detail.value;
|
||||
this._suggestName(previous);
|
||||
if (this._data?.name && !/^\w+$/.test(this._data.name)) {
|
||||
this._validationError.name = "invalid_name";
|
||||
}
|
||||
@@ -556,6 +564,32 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
);
|
||||
}
|
||||
|
||||
// Name a new disk mount after the partition's volume label, unless the user
|
||||
// typed a name of their own.
|
||||
private _suggestName(previous?: MountFormData) {
|
||||
const data = this._data;
|
||||
if (data?.name !== previous?.name) {
|
||||
this._suggestedName = undefined;
|
||||
}
|
||||
if (
|
||||
this._existing ||
|
||||
data?.type !== SupervisorMountType.DISK ||
|
||||
(data.device === previous?.device &&
|
||||
previous?.type === SupervisorMountType.DISK) ||
|
||||
(data.name && data.name !== this._suggestedName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const name = mountNameFromLabel(
|
||||
this._partitions?.find((entry) => entry.partition.device === data.device)
|
||||
?.partition.label
|
||||
);
|
||||
if (name !== (data.name ?? "")) {
|
||||
data.name = name;
|
||||
}
|
||||
this._suggestedName = name;
|
||||
}
|
||||
|
||||
private async _connectMount(ev) {
|
||||
const progressButton = ev.target as HaProgressButton;
|
||||
this._error = undefined;
|
||||
|
||||
@@ -18,12 +18,12 @@ import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-segmented-bar";
|
||||
import type { Segment } from "../../../components/ha-segmented-bar";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHostInfo, HostDisksUsage } from "../../../data/hassio/host";
|
||||
import {
|
||||
@@ -187,18 +187,18 @@ class HaConfigSectionStorage extends LitElement {
|
||||
}
|
||||
</ha-alert>`
|
||||
: validMounts?.length
|
||||
? html`<ha-list-base
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.storage.network_mounts.title"
|
||||
)}
|
||||
>
|
||||
? html`<ha-list>
|
||||
${validMounts.map(
|
||||
(mount) => html`
|
||||
<ha-list-item-button
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
.mount=${mount}
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
<div slot="start" class="mount-icon">
|
||||
<div slot="graphic">
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
mount.usage === SupervisorMountUsage.MEDIA
|
||||
@@ -210,12 +210,11 @@ class HaConfigSectionStorage extends LitElement {
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<span
|
||||
slot="headline"
|
||||
class="mount-state-${mount.state || "unknown"}"
|
||||
>
|
||||
${mount.name}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
<span slot="secondary">
|
||||
<span class="mount-address">
|
||||
${supervisorMountDescription(mount)}
|
||||
</span>
|
||||
@@ -225,17 +224,19 @@ class HaConfigSectionStorage extends LitElement {
|
||||
mount.state !== SupervisorMountState.ACTIVE
|
||||
? html`<ha-icon-button
|
||||
class="reload-btn"
|
||||
slot="end"
|
||||
slot="meta"
|
||||
.mount=${mount}
|
||||
@click=${this._reloadMount}
|
||||
.path=${mdiReload}
|
||||
></ha-icon-button>`
|
||||
: html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: html`<ha-icon-next
|
||||
slot="meta"
|
||||
></ha-icon-next>`
|
||||
}
|
||||
</ha-list-item-button>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>`
|
||||
</ha-list>`
|
||||
: html`<div class="no-mounts">
|
||||
<ha-svg-icon .path=${mdiNas}></ha-svg-icon>
|
||||
<p>
|
||||
@@ -489,6 +490,8 @@ class HaConfigSectionStorage extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* multiline-secondary lets the secondary slot wrap, so the address keeps its
|
||||
own single ellipsized line and only the usage sits below it. */
|
||||
.mount-address {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
@@ -557,20 +560,14 @@ class HaConfigSectionStorage extends LitElement {
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-item-meta-display: flex;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
.mount-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
}
|
||||
.mount-icon,
|
||||
.reload-btn,
|
||||
ha-list-item-button ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
|
||||
@@ -10,11 +10,12 @@ import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-selector/ha-selector-datetime";
|
||||
import "../../../../components/ha-selector/ha-selector-number";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { StatisticValue } from "../../../../data/recorder";
|
||||
import {
|
||||
adjustStatisticsSum,
|
||||
@@ -240,21 +241,25 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
|
||||
}
|
||||
);
|
||||
rows.push(html`
|
||||
<ha-list-item-button
|
||||
.headline=${change}
|
||||
.supportingText=${formatDateTime(
|
||||
new Date(stat.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
<ha-list-item
|
||||
twoline
|
||||
hasMeta
|
||||
.stat=${stat}
|
||||
@click=${this._setChosenStatistic}
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<span>${change}</span>
|
||||
<span slot="secondary">
|
||||
${formatDateTime(
|
||||
new Date(stat.start),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`);
|
||||
}
|
||||
stats = html`<ha-list-base>${rows}</ha-list-base>`;
|
||||
stats = html`${rows}`;
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -630,14 +635,9 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends DirtyStateProvid
|
||||
ha-selector-number {
|
||||
margin-bottom: var(--ha-space-5);
|
||||
}
|
||||
ha-list-base {
|
||||
ha-list-item {
|
||||
margin: 0 -24px;
|
||||
}
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
ha-list-item-button ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
.table-row {
|
||||
display: flex;
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { ContextType } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
@@ -78,7 +78,6 @@ import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import { animationStyles } from "../../../resources/theme/animations.globals";
|
||||
|
||||
const RANGE_KEYS: DateRange[] = [
|
||||
"today",
|
||||
@@ -151,6 +150,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _loadingLeaving = false;
|
||||
|
||||
@query(".loading-indicator") private _loadingIndicator?: HTMLElement;
|
||||
|
||||
private _loadingTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
@@ -379,10 +382,15 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._loading
|
||||
this._loading || this._loadingLeaving
|
||||
? html`<ha-spinner
|
||||
class="loading-indicator"
|
||||
class=${classMap({
|
||||
"loading-indicator": true,
|
||||
leaving: this._loadingLeaving,
|
||||
})}
|
||||
size="small"
|
||||
@transitionend=${this._loadingTransitionDone}
|
||||
@transitioncancel=${this._loadingTransitionDone}
|
||||
></ha-spinner>`
|
||||
: nothing
|
||||
}
|
||||
@@ -727,6 +735,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
private _updateDates(energyData: EnergyData): void {
|
||||
clearTimeout(this._loadingTimer);
|
||||
if (this._loading) {
|
||||
this._loadingLeaving = true;
|
||||
this._removeLoadingIndicatorWithoutTransition();
|
||||
}
|
||||
this._loading = false;
|
||||
this._compare = energyData.startCompare !== undefined;
|
||||
this._startDate = energyData.start;
|
||||
@@ -759,10 +771,30 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
// Basically to ensure there's no "flash loading" when data is loaded quickly
|
||||
clearTimeout(this._loadingTimer);
|
||||
this._loadingTimer = setTimeout(() => {
|
||||
this._loadingLeaving = false;
|
||||
this._loading = true;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private async _removeLoadingIndicatorWithoutTransition() {
|
||||
await this.updateComplete;
|
||||
// No transition starts when the spinner was still at opacity 0
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
|
||||
private _loadingTransitionDone(ev: TransitionEvent) {
|
||||
if (ev.propertyName === "opacity") {
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
private _removeIdleLoadingIndicator() {
|
||||
// Reversing the fade-in cancels a transition while the fade-out runs
|
||||
if (!this._loadingIndicator?.getAnimations().length) {
|
||||
this._loadingLeaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _getDatePickerPlacement = memoizeOne(
|
||||
(
|
||||
verticalOpeningDirection: VerticalOpeningDirection | undefined,
|
||||
@@ -778,7 +810,6 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
${animationStyles}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -843,7 +874,15 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
position: absolute;
|
||||
right: var(--ha-space-2);
|
||||
align-items: center;
|
||||
animation: fade-in var(--ha-animation-duration-normal) ease-in-out;
|
||||
transition: opacity var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
@starting-style {
|
||||
.loading-indicator {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-indicator.leaving {
|
||||
opacity: 0;
|
||||
}
|
||||
.date-actions .overflow {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LovelacePanelConfig } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
@@ -13,8 +14,10 @@ import {
|
||||
} from "../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
||||
import { fetchDashboards } from "../../../data/lovelace/dashboard";
|
||||
import { getPanelTitleFromUrlPath, LOVELACE_PANEL } from "../../../data/panel";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { SECTIONS_VIEW_LAYOUT } from "../views/const";
|
||||
import { getViewType } from "../views/get-view-type";
|
||||
import type { SuggestCardDialogParams } from "./card-editor/show-suggest-card-dialog";
|
||||
@@ -144,7 +147,23 @@ export const addEntitiesToLovelaceView = async (
|
||||
await saveConfig(hass!, viewUrlPath, newConfig);
|
||||
} catch (_err: any) {
|
||||
alert(hass.localize("ui.panel.lovelace.add_entities.saving_failed"));
|
||||
return;
|
||||
}
|
||||
const dashboardPath = viewUrlPath ?? LOVELACE_PANEL;
|
||||
const viewPath = newConfig.views[viewIndex].path || viewIndex;
|
||||
const dashboardTitle = getPanelTitleFromUrlPath(hass, dashboardPath);
|
||||
showToast(element, {
|
||||
message: dashboardTitle
|
||||
? {
|
||||
translationKey: "ui.panel.lovelace.add_entities.added",
|
||||
args: { dashboard: dashboardTitle },
|
||||
}
|
||||
: { translationKey: "ui.panel.lovelace.add_entities.added_unnamed" },
|
||||
action: {
|
||||
text: { translationKey: "ui.panel.lovelace.add_entities.open" },
|
||||
action: () => navigate(`/${dashboardPath}/${viewPath}`),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import type { DeviceSectionCategory } from "../../strategies/device/device-section-strategy";
|
||||
import {
|
||||
@@ -253,7 +252,6 @@ export class HuiDialogAddDeviceCard
|
||||
addCards(this._params.lovelaceConfig, this._params.path, this._cardConfig)
|
||||
);
|
||||
this._saving = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/vie
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
@@ -282,7 +281,6 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
);
|
||||
await this._params!.saveConfig(newConfig);
|
||||
this._saving = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import { computeUnusedEntities } from "../../common/compute-unused-entities";
|
||||
import {
|
||||
computeCards,
|
||||
@@ -111,6 +112,11 @@ export class HuiUnusedEntities extends LitElement {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _saveConfig = async (config: LovelaceConfig): Promise<void> => {
|
||||
await this.lovelace.saveConfig(config);
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
};
|
||||
|
||||
private _addToLovelaceView(): void {
|
||||
const cardConfig = computeCards(this.hass, this._selectedEntities, {});
|
||||
const sectionConfig = computeSection(this._selectedEntities, {});
|
||||
@@ -118,7 +124,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
if (this.lovelace.config.views.length === 1) {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
saveConfig: this._saveConfig,
|
||||
path: ["views", 0],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
@@ -132,7 +138,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
viewSelectedCallback: (_urlPath, _selectedDashConfig, viewIndex) => {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
saveConfig: this._saveConfig,
|
||||
path: ["views", viewIndex],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
} from "echarts/components";
|
||||
|
||||
// Features like Universal Transition and Label Layout
|
||||
@@ -48,6 +50,8 @@ import type {
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
VisualMapComponentOption,
|
||||
MarkAreaComponentOption,
|
||||
BrushComponentOption,
|
||||
} from "echarts/components";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
|
||||
@@ -62,6 +66,8 @@ export type ECOption = ComposeOption<
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| VisualMapComponentOption
|
||||
| MarkAreaComponentOption
|
||||
| BrushComponentOption
|
||||
| SankeySeriesOption
|
||||
| GraphSeriesOption
|
||||
| SunburstSeriesOption
|
||||
@@ -91,6 +97,8 @@ echarts.use([
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
]);
|
||||
|
||||
export { LinearGradient };
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Install the Intl.PluralRules polyfill together with its locale data.
|
||||
// FormatJS replaces Intl.PluralRules as soon as polyfill-force runs, and a
|
||||
// data-less polyfill throws from select(). Keeping both imports in one module
|
||||
// closes that gap for every caller, including the should-polyfill probes
|
||||
// that other entry bundles run.
|
||||
import "@formatjs/intl-pluralrules/polyfill-force";
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
import "@formatjs/intl-pluralrules/locale-data/en";
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
polyfillTimeZoneData,
|
||||
} from "./locale-data-polyfill";
|
||||
|
||||
let polyfilled = false;
|
||||
|
||||
const _polyfillTimeZoneData = polyfillTimeZoneData;
|
||||
|
||||
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
|
||||
@@ -47,10 +45,6 @@ const setDefaultTimeZone = () => {
|
||||
};
|
||||
|
||||
const polyfillIntl = async () => {
|
||||
if (polyfilled) {
|
||||
return;
|
||||
}
|
||||
polyfilled = true;
|
||||
const locale = getLocalLanguage();
|
||||
const polyfills: Promise<unknown>[] = [];
|
||||
if (shouldPolyfillGetCanonicalLocales()) {
|
||||
@@ -79,14 +73,10 @@ const polyfillIntl = async () => {
|
||||
polyfills.push(import("@formatjs/intl-numberformat/polyfill-force"));
|
||||
}
|
||||
if (shouldPolyfillPluralRules(locale)) {
|
||||
polyfills.push(
|
||||
import("@formatjs/intl-pluralrules/polyfill-force").then(
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
() => import("@formatjs/intl-pluralrules/locale-data/en")
|
||||
)
|
||||
);
|
||||
// The polyfill and its English data are one chunk, so Intl.PluralRules
|
||||
// is never installed without data. Its should-polyfill probe calls
|
||||
// select(), which throws on a data-less polyfill.
|
||||
polyfills.push(import("./intl-pluralrules"));
|
||||
}
|
||||
if (shouldPolyfillRelativeTimeFormat(locale)) {
|
||||
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill-force"));
|
||||
@@ -100,4 +90,9 @@ const polyfillIntl = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
await polyfillIntl();
|
||||
// Each legacy entry bundle (core, app, authorize, onboarding) carries its own
|
||||
// copy of this module, and the bundles load in parallel. A module-level flag
|
||||
// only stops re-runs within one bundle, so a second bundle could probe
|
||||
// Intl.* while the first was still loading locale data. Share one promise
|
||||
// per page instead, so every bundle waits for the same run.
|
||||
await (globalThis.__haIntlPolyfill ??= polyfillIntl());
|
||||
@@ -10233,7 +10233,10 @@
|
||||
"add_entities": {
|
||||
"yaml_unsupported": "You cannot use this function when using your dashboard in YAML mode.",
|
||||
"generated_unsupported": "You can only use this function when you have taken control of your dashboard.",
|
||||
"saving_failed": "Saving dashboard configuration failed."
|
||||
"saving_failed": "Saving dashboard configuration failed.",
|
||||
"added": "Added to {dashboard}",
|
||||
"added_unnamed": "Added to the dashboard",
|
||||
"open": "Open"
|
||||
},
|
||||
"views": {
|
||||
"unnamed_view": "Unnamed view",
|
||||
|
||||
@@ -41,6 +41,8 @@ declare global {
|
||||
var __HASS_URL__: string;
|
||||
// Read by intl-polyfill before a polyfill replaces Intl.DateTimeFormat
|
||||
var __haNativeTimeZone: string | undefined;
|
||||
// Shared by every legacy entry bundle so the Intl polyfills load once
|
||||
var __haIntlPolyfill: Promise<void> | undefined;
|
||||
// Handshake between the app entry and the extra modules in index.html
|
||||
var __haScopedRegistryReady: boolean | undefined;
|
||||
var __haLoadExtraModules: (() => void) | undefined;
|
||||
|
||||
Reference in new issue
Block a user