mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-13 04:36:53 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8af5908682 | |||
| 60e95b886c | |||
| 0385ca8076 | |||
| 02c65fc8cb |
@@ -13,7 +13,9 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
@@ -241,6 +243,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const chartIsBar = this.chartType.startsWith("bar");
|
||||
const period = this.period;
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
@@ -252,8 +256,67 @@ export class StatisticsChart extends LitElement {
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
}
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
@@ -265,14 +328,7 @@ export class StatisticsChart extends LitElement {
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -511,33 +567,53 @@ export class StatisticsChart extends LitElement {
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
|
||||
// Place bars at centre of their specified time range if this is a bar chart
|
||||
// and the period is 5minute or hour.
|
||||
const centerBars =
|
||||
chartType === "bar" &&
|
||||
(this.period === "5minute" || this.period === "hour");
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
start: Date, // Data point start time
|
||||
end: Date, // Data point end time
|
||||
limit: Date, // Limit for end time (e.g. now)
|
||||
dataValues: (number | null)[][]
|
||||
) => {
|
||||
if (!dataValues.length) return;
|
||||
if (start > end) {
|
||||
// Limit for time range is lesser of overall limit and data point end
|
||||
limit = end.getTime() < limit.getTime() ? end : limit;
|
||||
if (start.getTime() > limit.getTime()) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
statDataSets.forEach((d, i) => {
|
||||
if (
|
||||
chartType === "line" &&
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
if (chartType === "line") {
|
||||
if (
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
} else {
|
||||
let time = start;
|
||||
if (centerBars) {
|
||||
// If centering bars, set the time to the midpoint between start and end instead
|
||||
// of the start time.
|
||||
time = new Date((start.getTime() + end.getTime()) / 2);
|
||||
}
|
||||
// Data value should always be a scalar for bar charts. Pass in
|
||||
// real start time as extra value to allow formatting tooltip.
|
||||
d.data!.push([time, dataValues[i][0]!, start, end]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
prevEndTime = limit;
|
||||
};
|
||||
|
||||
let color = colors[statistic_id];
|
||||
@@ -697,11 +773,7 @@ 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, endDate, endTime, dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
/**
|
||||
* @element ha-entity-toggle
|
||||
*
|
||||
* @cssprop --ha-entity-toggle-switch-width - Width of the switch track. Defaults to `38px`.
|
||||
* @cssprop --ha-entity-toggle-switch-size - Height of the switch track. Defaults to `20px`.
|
||||
* @cssprop --ha-entity-toggle-switch-thumb-size - Size of the switch thumb. Defaults to `14px`.
|
||||
*/
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
@@ -165,9 +173,9 @@ export class HaEntityToggle extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-switch {
|
||||
--ha-switch-width: 38px;
|
||||
--ha-switch-size: 20px;
|
||||
--ha-switch-thumb-size: 14px;
|
||||
--ha-switch-width: var(--ha-entity-toggle-switch-width, 38px);
|
||||
--ha-switch-size: var(--ha-entity-toggle-switch-size, 20px);
|
||||
--ha-switch-thumb-size: var(--ha-entity-toggle-switch-thumb-size, 14px);
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 40px;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
import "../../ha-dialog";
|
||||
import "../../ha-adaptive-dialog";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
import "../../ha-icon-next";
|
||||
@@ -153,7 +153,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
!this._entitySourcesLoaded;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
@@ -187,7 +187,7 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
`}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import {
|
||||
mdiCheckCircle,
|
||||
mdiChip,
|
||||
mdiCircleOffOutline,
|
||||
mdiCursorDefaultClickOutline,
|
||||
mdiDocker,
|
||||
mdiExclamationThick,
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
mdiNumeric6,
|
||||
mdiNumeric7,
|
||||
mdiNumeric8,
|
||||
mdiPlayCircle,
|
||||
mdiPound,
|
||||
mdiShield,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -30,7 +31,6 @@ import { atLeastVersion } from "../../../../../common/config/version";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
||||
import type { LocalizeKeys } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../../components/chips/ha-chip-set";
|
||||
@@ -79,9 +79,9 @@ import { bytesToString } from "../../../../../util/bytes-to-string";
|
||||
import { getAppDisplayName } from "../../common/app";
|
||||
import "../../components/supervisor-apps-card-content";
|
||||
import "../components/supervisor-app-metric";
|
||||
import "../components/supervisor-app-update-available-card";
|
||||
import { extractChangelog } from "../util/supervisor-app";
|
||||
import "./supervisor-app-system-managed";
|
||||
import "../components/supervisor-app-update-available-card";
|
||||
|
||||
const STAGE_ICON = {
|
||||
stable: mdiCheckCircle,
|
||||
@@ -203,10 +203,28 @@ class SupervisorAppInfo extends LitElement {
|
||||
: nothing}
|
||||
<div class="addon-version light-color">
|
||||
${this.addon.version
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this.addon.state}
|
||||
></supervisor-apps-state>`
|
||||
: this.addon.version_latest}
|
||||
? html`
|
||||
${this._computeIsRunning
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.app_running"
|
||||
)}
|
||||
class="running"
|
||||
.path=${mdiPlayCircle}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.app_stopped"
|
||||
)}
|
||||
class="stopped"
|
||||
.path=${mdiCircleOffOutline}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
`
|
||||
: html` ${this.addon.version_latest} `}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description light-color">
|
||||
@@ -819,7 +837,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
const id = ev.currentTarget.id as AddonCapability;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.${id}.title` as LocalizeKeys
|
||||
`ui.panel.config.apps.dashboard.capability.${id}.title`
|
||||
),
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.${id}.description`
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
|
||||
import type { AddonStage } from "../../../../data/hassio/addon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAppDisplayName } from "../common/app";
|
||||
import "./supervisor-apps-state";
|
||||
import "./supervisor-apps-tag";
|
||||
|
||||
export interface AppTag {
|
||||
label: string;
|
||||
variant: "brand" | "success" | "warning" | "danger" | "neutral";
|
||||
iconPath?: string;
|
||||
}
|
||||
|
||||
@customElement("supervisor-apps-card-content")
|
||||
class SupervisorAppsCardContent extends LitElement {
|
||||
@@ -25,13 +16,13 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public stage: AddonStage = "stable";
|
||||
|
||||
@property() public state: AddonState = null;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ type: Boolean }) public available = true;
|
||||
|
||||
@property({ attribute: false }) public tags?: AppTag[];
|
||||
@property({ attribute: false }) public showTopbar = false;
|
||||
|
||||
@property({ attribute: false }) public topbarClass?: string;
|
||||
|
||||
@property({ attribute: false }) public iconTitle?: string;
|
||||
|
||||
@@ -42,87 +33,78 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="app">
|
||||
<div class="icon-wrapper">
|
||||
${this.iconImage
|
||||
? html`
|
||||
<img
|
||||
class="icon-image"
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
class="app-icon"
|
||||
.path=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
</div>
|
||||
<div>
|
||||
<div class="title-row">
|
||||
<div class="title">
|
||||
${getAppDisplayName(this.title, this.stage)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${
|
||||
/* treat as available when undefined */
|
||||
this.available === false ? " (Not available)" : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.tags?.length || this.state
|
||||
? html`
|
||||
<div class="footer">
|
||||
<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>
|
||||
const stageLabel =
|
||||
this.stage !== "stable"
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${this.stage}`
|
||||
)
|
||||
: undefined;
|
||||
|
||||
${this.tags?.length
|
||||
? html`<div class="tags">
|
||||
${this.tags.map(
|
||||
(tag) =>
|
||||
html`<supervisor-apps-tag
|
||||
.variant=${tag.variant}
|
||||
.iconPath=${tag.iconPath}
|
||||
.label=${tag.label}
|
||||
></supervisor-apps-tag>`
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
return html`
|
||||
${this.showTopbar
|
||||
? html` <div class="topbar ${this.topbarClass}"></div> `
|
||||
: ""}
|
||||
${this.iconImage
|
||||
? html`
|
||||
<div class="icon_image ${this.iconClass}">
|
||||
<img
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
class=${this.iconClass!}
|
||||
.path=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<div>
|
||||
<div class="title-row">
|
||||
<div class="title">${getAppDisplayName(this.title, this.stage)}</div>
|
||||
${stageLabel
|
||||
? html` <span class="stage ${this.stage}"> ${stageLabel} </span> `
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${
|
||||
/* treat as available when undefined */
|
||||
this.available === false ? " (Not available)" : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.app {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
gap: var(--ha-space-4);
|
||||
display: flex;
|
||||
:host {
|
||||
direction: ltr;
|
||||
}
|
||||
.icon-wrapper {
|
||||
position: relative;
|
||||
margin-top: var(--ha-space-1);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.app-icon {
|
||||
|
||||
ha-svg-icon {
|
||||
margin-right: var(--ha-space-6);
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-3);
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon-image {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
ha-svg-icon.update {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
ha-svg-icon.running,
|
||||
ha-svg-icon.installed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
ha-svg-icon.hassupdate,
|
||||
ha-svg-icon.backup {
|
||||
color: var(--state-icon-color);
|
||||
}
|
||||
ha-svg-icon.not_available {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
@@ -138,6 +120,22 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
gap: var(--ha-space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
.stage {
|
||||
flex: none;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 4px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stage.experimental {
|
||||
color: var(--warning-color);
|
||||
background-color: rgba(var(--rgb-warning-color), 0.12);
|
||||
}
|
||||
.stage.deprecated {
|
||||
color: var(--error-color);
|
||||
background-color: rgba(var(--rgb-error-color), 0.12);
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: var(--ha-space-1);
|
||||
@@ -146,18 +144,43 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
height: 2.4em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
.footer {
|
||||
border-top: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-quiet);
|
||||
padding-top: var(--ha-space-2);
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
margin-top: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-4);
|
||||
float: left;
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
.icon_image.stopped,
|
||||
.icon_image.not_available {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
background-color: var(--warning-color);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: var(--ha-space-2);
|
||||
right: var(--ha-space-2);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.topbar.installed {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.topbar.update {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.topbar.unavailable {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import type { AddonState } from "../../../../data/hassio/addon";
|
||||
|
||||
@customElement("supervisor-apps-state")
|
||||
class SupervisorAppsState extends LitElement {
|
||||
@property() public state: Exclude<AddonState, null> = "unknown";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="dot state-${this.state}"></div>
|
||||
<span
|
||||
>${this._i18n.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
|
||||
)}</span
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--ha-color-on-neutral-normal);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot.state-started {
|
||||
background-color: var(--ha-color-on-success-normal);
|
||||
animation: state-dot-pulse 1.8s infinite;
|
||||
}
|
||||
.dot.state-startup {
|
||||
background-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
.dot.state-error {
|
||||
background-color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
@keyframes state-dot-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(var(--rgb-success-color), 0.6);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(var(--rgb-success-color), 0);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion) {
|
||||
.dot.state-started {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-apps-state": SupervisorAppsState;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
|
||||
@customElement("supervisor-apps-tag")
|
||||
class SupervisorAppsTag extends LitElement {
|
||||
@property() public variant:
|
||||
| "brand"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "neutral" = "neutral";
|
||||
|
||||
@property({ attribute: "icon-path" }) public iconPath?: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<wa-tag .variant=${this.variant}>
|
||||
${this.iconPath
|
||||
? html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`
|
||||
: nothing}
|
||||
${this.label}
|
||||
</wa-tag>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
wa-tag {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
height: 20px;
|
||||
border: none;
|
||||
padding-inline: var(--ha-space-1) var(--ha-space-2);
|
||||
}
|
||||
wa-tag ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
wa-tag[variant="success"] {
|
||||
color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
wa-tag[variant="warning"] {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
wa-tag[variant="danger"] {
|
||||
color: var(--ha-color-on-error-normal);
|
||||
}
|
||||
wa-tag[variant="neutral"] {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
wa-tag[variant="brand"] {
|
||||
color: var(--ha-color-on-primary-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-apps-tag": SupervisorAppsTag;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
mdiAlertDecagramOutline,
|
||||
mdiArrowUpBoldCircle,
|
||||
mdiArrowUpBoldCircleOutline,
|
||||
mdiFlask,
|
||||
mdiPuzzle,
|
||||
mdiRefresh,
|
||||
mdiStorePlus,
|
||||
@@ -32,9 +29,7 @@ import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { getAppDisplayName } from "./common/app";
|
||||
import "./components/supervisor-apps-card-content";
|
||||
import type { AppTag } from "./components/supervisor-apps-card-content";
|
||||
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
|
||||
|
||||
@customElement("ha-config-apps-installed")
|
||||
@@ -101,59 +96,65 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
</ha-input-search>
|
||||
</div>
|
||||
<div class="content">
|
||||
${addons.length === 0
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<button class="link" @click=${this._openStore}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.installed.no_apps"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: addons.map(
|
||||
(addon) => html`
|
||||
<ha-card
|
||||
role="button"
|
||||
tabindex="0"
|
||||
outlined
|
||||
.addon=${addon}
|
||||
@click=${this._addonTapped}
|
||||
aria-label=${getAppDisplayName(addon.name, addon.stage)}
|
||||
>
|
||||
<div class="card-group">
|
||||
${addons.length === 0
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<supervisor-apps-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.stage=${addon.stage}
|
||||
.description=${addon.description}
|
||||
available
|
||||
.tags=${this._getAppTags(addon)}
|
||||
.state=${addon.state}
|
||||
.icon=${addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_stopped"
|
||||
)
|
||||
: addon.update_available
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_update_available"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_running"
|
||||
)}
|
||||
.iconImage=${addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></supervisor-apps-card-content>
|
||||
<button class="link" @click=${this._openStore}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.installed.no_apps"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
: addons.map(
|
||||
(addon) => html`
|
||||
<ha-card
|
||||
outlined
|
||||
.addon=${addon}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<supervisor-apps-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.stage=${addon.stage}
|
||||
.description=${addon.description}
|
||||
available
|
||||
.showTopbar=${addon.update_available}
|
||||
topbarClass="update"
|
||||
.icon=${addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_stopped"
|
||||
)
|
||||
: addon.update_available
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_update_available"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.installed.app_running"
|
||||
)}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ha-button size="large" href="/config/apps/available">
|
||||
@@ -216,32 +217,6 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
navigate("/config/apps/available");
|
||||
}
|
||||
|
||||
private _getAppTags(addon: HassioAddonInfo): AppTag[] {
|
||||
const labels: AppTag[] = [];
|
||||
|
||||
if (addon.update_available) {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.state.update_available`
|
||||
),
|
||||
variant: "brand",
|
||||
iconPath: mdiArrowUpBoldCircleOutline,
|
||||
});
|
||||
}
|
||||
if (addon.stage !== "stable") {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
|
||||
),
|
||||
variant: addon.stage === "experimental" ? "warning" : "danger",
|
||||
iconPath:
|
||||
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
supervisorAppsStyle,
|
||||
css`
|
||||
@@ -254,10 +229,7 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-card:hover {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.search {
|
||||
@@ -275,13 +247,10 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
.content {
|
||||
padding: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-18);
|
||||
gap: var(--ha-space-4);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(336px, 100%), 1fr));
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
button.link {
|
||||
|
||||
+4
-1
@@ -866,10 +866,13 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
undefined
|
||||
);
|
||||
|
||||
const filteredFloors = this._floorAreas.filter(
|
||||
({ id, areas }) => id !== undefined && areas.length
|
||||
);
|
||||
this._floorAreas.forEach((floor) => {
|
||||
this._entries[floor.id || `floor${TARGET_SEPARATOR}`] = {
|
||||
// auto expand if only one floor is present
|
||||
open: this._floorAreas.length === 1,
|
||||
open: filteredFloors.length === 1 && filteredFloors[0].id === floor.id,
|
||||
areas: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ import "./ha-domain-integrations";
|
||||
import "./ha-integration-list-item";
|
||||
import type { AddIntegrationDialogParams } from "./show-add-integration-dialog";
|
||||
import { showYamlIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showSingleConfigEntryWarning } from "./show-single-config-entry-warning";
|
||||
|
||||
export interface IntegrationListItem {
|
||||
name: string;
|
||||
@@ -710,21 +711,8 @@ class AddIntegrationDialog extends LitElement {
|
||||
});
|
||||
if (configEntries.length > 0) {
|
||||
this.closeDialog();
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
integration.name
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, integration.name),
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
showSingleConfigEntryWarning(this, { domain: integration.domain });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-button";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import type { SingleConfigEntryWarningDialogParams } from "./show-single-config-entry-warning";
|
||||
|
||||
@customElement("dialog-single-config-entry-warning")
|
||||
class DialogSingleConfigEntryWarning extends DialogMixin<SingleConfigEntryWarningDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _backendLocalize?: LocalizeFunc;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadBackendLocalize();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._backendLocalize) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.headerTitle=${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
)}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: html`<b
|
||||
>${domainToName(this._backendLocalize, this.params.domain)}</b
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._i18n.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.href=${`/config/integrations/integration/${this.params.domain}`}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.show_integration"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadBackendLocalize() {
|
||||
if (!this.params) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._backendLocalize = await this._i18n.loadBackendTranslation(
|
||||
"title",
|
||||
this.params.domain
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-single-config-entry-warning": DialogSingleConfigEntryWarning;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +621,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${this._manifest?.integration_type !== "hardware"
|
||||
${this._manifest?.integration_type !== "hardware" &&
|
||||
(!this._manifest?.single_config_entry ||
|
||||
(normalData.length === 0 && attentionData.length === 0))
|
||||
? html`<ha-button
|
||||
.appearance=${canAddDevice ? "filled" : "accent"}
|
||||
@click=${this._addIntegration}
|
||||
@@ -1235,30 +1237,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this._manifest?.single_config_entry) {
|
||||
const entries = this._domainConfigEntries(
|
||||
this.domain,
|
||||
this._extraConfigEntries || this.configEntries
|
||||
);
|
||||
if (entries.length > 0) {
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
this._manifest.name
|
||||
);
|
||||
await showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, this._manifest.name),
|
||||
}
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
showAddIntegrationDialog(this, {
|
||||
domain: this.domain,
|
||||
navigateToResult: true,
|
||||
|
||||
@@ -69,6 +69,7 @@ import "./ha-integration-card";
|
||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||
import "./ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { showSingleConfigEntryWarning } from "./show-single-config-entry-warning";
|
||||
|
||||
export interface ConfigEntryExtended extends Omit<ConfigEntry, "entry_id"> {
|
||||
entry_id?: string;
|
||||
@@ -914,21 +915,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
if (integration.single_config_entry) {
|
||||
const configEntries = await getConfigEntries(this.hass, { domain });
|
||||
if (configEntries.length > 0) {
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
integration.name
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.single_config_entry",
|
||||
{
|
||||
integration_name: domainToName(localize, integration.name!),
|
||||
}
|
||||
),
|
||||
});
|
||||
showSingleConfigEntryWarning(this, { domain });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface SingleConfigEntryWarningDialogParams {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export const showSingleConfigEntryWarning = (
|
||||
element: LitElement,
|
||||
params: SingleConfigEntryWarningDialogParams
|
||||
) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-single-config-entry-warning",
|
||||
dialogParams: params,
|
||||
dialogImport: () => import("./dialog-single-config-entry-warning"),
|
||||
});
|
||||
@@ -2924,13 +2924,6 @@
|
||||
"experimental": "Experimental",
|
||||
"deprecated": "Deprecated"
|
||||
},
|
||||
"state": {
|
||||
"started": "Running",
|
||||
"stopped": "Stopped",
|
||||
"error": "Error",
|
||||
"startup": "Starting",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"label": {
|
||||
"rating": "Rating",
|
||||
"host": "Host",
|
||||
@@ -6917,6 +6910,7 @@
|
||||
"supported_hardware": "supported hardware",
|
||||
"proceed": "Proceed",
|
||||
"single_config_entry_title": "This integration allows only one configuration",
|
||||
"show_integration": "Show integration",
|
||||
"single_config_entry": "{integration_name} supports only one configuration. Adding additional ones is not needed."
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user