Compare commits

..

1 Commits

Author SHA1 Message Date
Zack
43dcd75be6 Fix Breaking Icon with new MDI Icons 2022-07-21 15:00:07 -05:00
41 changed files with 680 additions and 1198 deletions

View File

@@ -26,7 +26,7 @@ import {
import {
UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL,
} from "../../../src/panels/config/repairs/dialog-system-information";
} from "../../../src/panels/config/system-health/ha-config-system-health";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20220727.0"
version = "20220707.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -37,6 +37,7 @@ import {
mdiLightningBolt,
mdiMailbox,
mdiMapMarkerRadius,
mdiMicrophone,
mdiMolecule,
mdiMoleculeCo,
mdiMoleculeCo2,
@@ -47,7 +48,6 @@ import {
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiMicrophoneMessage,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
@@ -74,7 +74,7 @@ export const FIXED_DOMAIN_ICONS = {
camera: mdiVideo,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiMicrophoneMessage,
conversation: mdiMicrophone,
counter: mdiCounter,
demo: mdiHomeAssistant,
fan: mdiFan,

View File

@@ -1,88 +0,0 @@
import { html } from "lit";
import { getConfigEntries } from "../../data/config_entries";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";
import { fireEvent } from "../dom/fire_event";
import { navigate } from "../navigate";
export const protocolIntegrationPicked = async (
element: HTMLElement,
hass: HomeAssistant,
slug: string
) => {
if (slug === "zwave_js") {
const entries = await getConfigEntries(hass, {
domain: "zwave_js",
});
if (!entries.length) {
// If the component isn't loaded, ask them to load the integration first
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Z-Wave",
supported_hardware_link: html`<a
href=${documentationUrl(hass, "/docs/z-wave/controllers")}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zwave_js",
});
},
});
return;
}
showZWaveJSAddNodeDialog(element, {
entry_id: entries[0].entry_id,
});
} else if (slug === "zha") {
// If the component isn't loaded, ask them to load the integration first
if (!isComponentLoaded(hass, "zha")) {
showConfirmationDialog(element, {
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Zigbee",
supported_hardware_link: html`<a
href=${documentationUrl(
hass,
"/integrations/zha/#known-working-zigbee-radio-modules"
)}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(element, "handler-picked", {
handler: "zha",
});
},
});
return;
}
navigate("/config/zha/add");
}
};

View File

@@ -15,6 +15,8 @@ type LocalizeKeyExceptions =
| `state.${string}`
| `state_attributes.${string}`
| `state_badge.${string}`
| `groups.${string}`
| `config_entry.${string}`
| `ui.${string}`
| `${keyof TranslationDict["supervisor"]}.${string}`
| `component.${string}`;

View File

@@ -52,11 +52,6 @@ export class HaSelect extends SelectBase {
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: 12px;
padding-inline-end: 0px;

View File

@@ -21,7 +21,6 @@ import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
@@ -45,9 +44,7 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { updateCanInstall, UpdateEntity } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -180,7 +177,7 @@ const computePanels = memoizeOne(
let Sortable;
@customElement("ha-sidebar")
class HaSidebar extends SubscribeMixin(LitElement) {
class HaSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@@ -195,8 +192,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@state() private _updatesCount = 0;
@state() private _issuesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number;
@@ -219,16 +214,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _sortable?;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._issuesCount = repairs.issues.filter(
(issue) => !issue.ignored
).length;
}),
];
}
protected render() {
if (!this.hass) {
return html``;
@@ -253,7 +238,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -516,7 +500,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _renderConfiguration(title: string | null) {
return html`<a
return html` <a
class="configuration-container"
role="option"
href="/config"
@@ -527,20 +511,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
>
<paper-icon-item class="configuration" role="option">
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
${!this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge" slot="item-icon">
${this._updatesCount + this._issuesCount}
${this._updatesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
${this.alwaysExpand && this._updatesCount > 0
? html`
<span class="configuration-badge"
>${this._updatesCount + this._issuesCount}</span
>
<span class="configuration-badge">${this._updatesCount}</span>
`
: ""}
</paper-icon-item>

View File

@@ -83,7 +83,7 @@ export class HaTextField extends TextFieldBase {
}
input {
text-align: var(--text-field-text-align, start);
text-align: var(--text-field-text-align);
}
/* Chrome, Safari, Edge, Opera */

View File

@@ -13,8 +13,8 @@ export interface EntityRegistryEntry {
config_entry_id: string | null;
device_id: string | null;
area_id: string | null;
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
disabled_by: string | null;
hidden_by: string | null;
entity_category: "config" | "diagnostic" | null;
has_entity_name: boolean;
original_name?: string;

View File

@@ -1,9 +1,5 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type { DataEntryFlowStep } from "./data_entry_flow";
import { DataEntryFlowStep } from "./data_entry_flow";
export interface RepairsIssue {
domain: string;
@@ -26,21 +22,19 @@ export const severitySort = {
warning: 3,
};
export const fetchRepairsIssues = (conn: Connection) =>
conn.sendMessagePromise<{ issues: RepairsIssue[] }>({
export const fetchRepairsIssues = async (hass: HomeAssistant) =>
hass.callWS<{ issues: RepairsIssue[] }>({
type: "repairs/list_issues",
});
export const ignoreRepairsIssue = async (
export const dismissRepairsIssue = async (
hass: HomeAssistant,
issue: RepairsIssue,
ignore: boolean
issue: RepairsIssue
) =>
hass.callWS<string>({
type: "repairs/ignore_issue",
type: "repairs/dismiss_issue",
issue_id: issue.issue_id,
domain: issue.domain,
ignore,
});
export const createRepairsFlow = (
@@ -65,31 +59,3 @@ export const handleRepairsFlowStep = (
export const deleteRepairsFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `repairs/issues/fix/${flowId}`);
const subscribeRepairsIssueUpdates = (
conn: Connection,
store: Store<{ issues: RepairsIssue[] }>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchRepairsIssues(conn).then((repairs) =>
store.setState(repairs, true)
),
500,
true
),
"repairs_issue_registry_updated"
);
export const subscribeRepairsIssueRegistry = (
conn: Connection,
onChange: (repairs: { issues: RepairsIssue[] }) => void
) =>
createCollection<{ issues: RepairsIssue[] }>(
"_repairsIssueRegistry",
fetchRepairsIssues,
subscribeRepairsIssueUpdates,
conn,
onChange
);

View File

@@ -1,4 +1,3 @@
import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler";
import type { HomeAssistant } from "../types";
export type SupportedBrandHandler = Record<string, string>;
@@ -7,21 +6,3 @@ export const getSupportedBrands = (hass: HomeAssistant) =>
hass.callWS<Record<string, SupportedBrandHandler>>({
type: "supported_brands",
});
export const getSupportedBrandsLookup = (
supportedBrands: Record<string, SupportedBrandHandler>
): Record<string, Partial<SupportedBrandObj>> => {
const supportedBrandsIntegrations: Record<
string,
Partial<SupportedBrandObj>
> = {};
for (const [d, domainBrands] of Object.entries(supportedBrands)) {
for (const [slug, name] of Object.entries(domainBrands)) {
supportedBrandsIntegrations[slug] = {
name,
supported_flows: [d],
};
}
}
return supportedBrandsIntegrations;
};

View File

@@ -4,7 +4,7 @@ import {
mdiHomeCircleOutline,
mdiCancel,
} from "@mdi/js";
import { HomeAssistant, TranslationDict } from "../types";
import { HomeAssistant } from "../types";
import { Credential } from "./auth";
export const SYSTEM_GROUP_ID_ADMIN = "system-admin";
@@ -21,7 +21,7 @@ export interface User {
is_active: boolean;
local_only: boolean;
system_generated: boolean;
group_ids: (keyof TranslationDict["groups"])[];
group_ids: string[];
credentials: Credential[];
}
@@ -95,12 +95,7 @@ export const computeUserBadges = (
includeSystem: boolean
) => {
const labels: [string, string][] = [];
const translate = (
key: Extract<
keyof TranslationDict["ui"]["panel"]["config"]["users"],
`is_${string}`
>
) => hass.localize(`ui.panel.config.users.${key}`);
const translate = (key) => hass.localize(`ui.panel.config.users.${key}`);
if (user.is_owner) {
labels.push([OWNER_ICON, translate("is_owner")]);

View File

@@ -14,13 +14,14 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize";
import "../../components/ha-icon-next";
import "../../components/search-input";
import { getConfigEntries } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
@@ -35,7 +36,7 @@ interface HandlerObj {
is_helper?: boolean;
}
export interface SupportedBrandObj extends HandlerObj {
interface SupportedBrandObj extends HandlerObj {
supported_flows: string[];
}
@@ -299,7 +300,79 @@ class StepFlowPickHandler extends LitElement {
}
private async _handleAddPicked(slug: string): Promise<void> {
await protocolIntegrationPicked(this, this.hass, slug);
if (slug === "zwave_js") {
const entries = await getConfigEntries(this.hass, {
domain: "zwave_js",
});
if (!entries.length) {
// If the component isn't loaded, ask them to load the integration first
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Z-Wave",
supported_hardware_link: html`<a
href=${documentationUrl(this.hass, "/docs/z-wave/controllers")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: this.hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(this, "handler-picked", {
handler: "zwave_js",
});
},
});
return;
}
showZWaveJSAddNodeDialog(this, {
entry_id: entries[0].entry_id,
});
} else if (slug === "zha") {
// If the component isn't loaded, ask them to load the integration first
if (!isComponentLoaded(this.hass, "zha")) {
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
{
integration: "Zigbee",
supported_hardware_link: html`<a
href=${documentationUrl(
this.hass,
"/integrations/zha/#known-working-zigbee-radio-modules"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: this.hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
fireEvent(this, "handler-picked", {
handler: "zha",
});
},
});
return;
}
navigate("/config/zha/add");
}
// This closes dialog.
fireEvent(this, "flow-update");
}

View File

@@ -206,7 +206,6 @@ class MoreInfoMediaPlayer extends LitElement {
flex-wrap: wrap;
align-items: center;
--mdc-theme-primary: currentColor;
direction: ltr;
}
.basic-controls {
@@ -214,15 +213,6 @@ class MoreInfoMediaPlayer extends LitElement {
flex-grow: 1;
}
.volume {
direction: ltr;
}
.source-input,
.sound-input {
direction: var(--direction);
}
.volume,
.source-input,
.sound-input {
@@ -235,9 +225,6 @@ class MoreInfoMediaPlayer extends LitElement {
.sound-input ha-select {
margin-left: 10px;
flex-grow: 1;
margin-inline-start: 10px;
margin-inline-end: initial;
direction: var(--direction);
}
.tts {

View File

@@ -329,9 +329,6 @@ export class HaVoiceCommandDialog extends LitElement {
ha-icon-button {
color: var(--secondary-text-color);
margin-right: -24px;
margin-inline-end: -24px;
margin-inline-start: initial;
direction: var(--direction);
}
ha-icon-button[active] {
@@ -376,25 +373,19 @@ export class HaVoiceCommandDialog extends LitElement {
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
float: right;
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
float: var(--float-start);
float: left;
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
.message a {

View File

@@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCloudLock, mdiDotsVertical, mdiMagnify } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntities } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@@ -24,9 +24,9 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
import { CloudStatus } from "../../../data/cloud";
import {
fetchRepairsIssues,
RepairsIssue,
severitySort,
subscribeRepairsIssueRegistry,
} from "../../../data/repairs";
import {
checkForEntityUpdates,
@@ -36,7 +36,6 @@ import {
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import "../../../layouts/ha-app-layout";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -111,7 +110,7 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
};
@customElement("ha-config-dashboard")
class HaConfigDashboard extends SubscribeMixin(LitElement) {
class HaConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
@@ -145,27 +144,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
return [...pages, ...configSections.dashboard];
});
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
const repairsIssues = repairs.issues.filter((issue) => !issue.ignored);
this._repairsIssues = {
issues: repairsIssues
.sort((a, b) => severitySort[a.severity] - severitySort[b.severity])
.slice(0, repairsIssues.length === 3 ? repairsIssues.length : 2),
total: repairsIssues.length,
};
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues.issues) {
integrations.add(issue.domain);
}
this.hass.loadBackendTranslation("issues", [...integrations]);
}),
];
}
protected render(): TemplateResult {
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesWithInstall(this.hass.states);
@@ -210,58 +188,53 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
.isWide=${this.isWide}
full-width
>
${repairsIssues.length || canInstallUpdates.length
? html`<ha-card outlined>
${repairsIssues.length
? html`
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<a class="button" href="/config/repairs">
${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count:
totalRepairIssues - repairsIssues.length,
}
)}
</a>
`
: ""}
`
: ""}
${repairsIssues.length && canInstallUpdates.length
? html`<hr />`
: ""}
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<a class="button" href="/config/updates">
${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count:
totalUpdates - canInstallUpdates.length,
}
)}
</a>
`
: ""}
`
: ""}
</ha-card>`
${repairsIssues.length
? html`
<ha-card outlined>
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalRepairIssues}
.repairsIssues=${repairsIssues}
></ha-config-repairs>
${totalRepairIssues > repairsIssues.length
? html`
<a class="button" href="/config/repairs">
${this.hass.localize(
"ui.panel.config.repairs.more_repairs",
{
count: totalRepairIssues - repairsIssues.length,
}
)}
</a>
`
: ""}
</ha-card>
`
: ""}
${canInstallUpdates.length
? html`
<ha-card outlined>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
<a class="button" href="/config/updates">
${this.hass.localize(
"ui.panel.config.updates.more_updates",
{
count: totalUpdates - canInstallUpdates.length,
}
)}
</a>
`
: ""}
</ha-card>
`
: ""}
<ha-card outlined>
@@ -281,6 +254,11 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
`;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._fetchIssues();
}
protected override updated(changedProps: PropertyValues): void {
super.updated(changedProps);
@@ -300,6 +278,23 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
}
);
private async _fetchIssues(): Promise<void> {
const repairsIssues = (await fetchRepairsIssues(this.hass)).issues;
this._repairsIssues = {
issues: repairsIssues
.sort((a, b) => severitySort[a.severity] - severitySort[b.severity])
.slice(0, repairsIssues.length === 3 ? repairsIssues.length : 2),
total: repairsIssues.length,
};
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues.issues) {
integrations.add(issue.domain);
}
this.hass.loadBackendTranslation("issues", [...integrations]);
}
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: true,
@@ -338,12 +333,9 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
color: var(--primary-text-color);
}
a.button {
display: inline-block;
color: var(--primary-text-color);
padding: 6px 16px;
margin: 8px 16px 16px 16px;
border-radius: 32px;
border: 1px solid var(--divider-color);
display: block;
color: var(--primary-color);
padding: 16px;
}
.title {
font-size: 16px;
@@ -373,16 +365,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
.keep-together {
display: inline-block;
}
hr {
height: 1px;
background-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border: none;
margin-top: 0;
}
`,
];
}

View File

@@ -6,14 +6,10 @@ import "../../../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
import {
computeDeviceName,
DeviceRegistryEntry,
} from "../../../../data/device_registry";
import { computeDeviceName } from "../../../../data/device_registry";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
import "../../../../components/ha-alert";
@customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement {
@@ -25,11 +21,11 @@ class DialogDeviceRegistryDetail extends LitElement {
@state() private _params?: DeviceRegistryDetailDialogParams;
@state() private _areaId!: string;
@property() public _areaId?: string | null;
@state() private _disabledBy!: DeviceRegistryEntry["disabled_by"];
@state() private _disabledBy!: string | null;
@state() private _submitting = false;
@state() private _submitting?: boolean;
public async showDialog(
params: DeviceRegistryDetailDialogParams
@@ -37,7 +33,7 @@ class DialogDeviceRegistryDetail extends LitElement {
this._params = params;
this._error = undefined;
this._nameByUser = this._params.device.name_by_user || "";
this._areaId = this._params.device.area_id || "";
this._areaId = this._params.device.area_id;
this._disabledBy = this._params.device.disabled_by;
await this.updateComplete;
}

View File

@@ -13,7 +13,6 @@ import {
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
updateEntityRegistryEntry,
@@ -26,7 +25,7 @@ import type { HomeAssistant } from "../../../types";
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@property() public entry!: ExtEntityRegistryEntry;
@state() private _origEntityId!: string;
@@ -34,7 +33,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _disabledBy!: string | null;
@state() private _hiddenBy!: string | null;
@@ -42,7 +41,7 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@state() private _device?: DeviceRegistryEntry;
@state() private _submitting = false;
@state() private _submitting?: boolean;
public async updateEntry(): Promise<void> {
this._submitting = true;
@@ -146,8 +145,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId || undefined}
.placeholder=${this._device?.area_id || undefined}
.value=${this._areaId}
.placeholder=${this._device?.area_id}
@value-changed=${this._areaPicked}
></ha-area-picker>
@@ -183,8 +182,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -201,8 +200,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -219,8 +218,8 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
.disabled=${this._device?.disabled_by ||
(this._disabledBy &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
@@ -303,9 +302,3 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-registry-basic-editor": HaEntityRegistryBasicEditor;
}
}

View File

@@ -53,7 +53,6 @@ import {
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
fetchEntityRegistry,
@@ -129,7 +128,7 @@ const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
@property() public entry!: ExtEntityRegistryEntry;
@state() private _name!: string;
@@ -143,9 +142,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _areaId?: string | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _disabledBy!: string | null;
@state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];
@state() private _hiddenBy!: string | null;
@state() private _device?: DeviceRegistryEntry;
@@ -631,10 +630,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${(this._hiddenBy !== null &&
this._hiddenBy !== "user") ||
!!this._device?.disabled_by ||
(this._disabledBy !== null &&
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
this._device?.disabled_by ||
(this._disabledBy &&
this._disabledBy !== "user" &&
this._disabledBy !== "integration")}
@change=${this._viewStatusChanged}

View File

@@ -15,7 +15,6 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -86,11 +85,11 @@ export interface EntityRow extends StateEntity {
export class HaConfigEntities extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@property() public isWide!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property() public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property() public route!: Route;
@state() private _entities?: EntityRegistryEntry[];
@@ -175,7 +174,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon",
template: (_, entry: EntityRow) => html`
<ha-state-icon
title=${ifDefined(entry.entity?.state)}
.title=${entry.entity?.state}
slot="item-icon"
.state=${entry.entity}
></ha-state-icon>
@@ -238,10 +237,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
hidden: narrow || !showDisabled,
filterable: true,
width: "15%",
template: (disabled_by: EntityRegistryEntry["disabled_by"]) =>
disabled_by === null
? "—"
: this.hass.localize(`config_entry.disabled_by.${disabled_by}`),
template: (disabled_by) =>
this.hass.localize(
`ui.panel.config.devices.disabled_by.${disabled_by}`
) ||
disabled_by ||
"—",
},
status: {
title: this.hass.localize(
@@ -1011,9 +1012,3 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-entities": HaConfigEntities;
}
}

View File

@@ -6,6 +6,7 @@ import {
mdiCog,
mdiDatabase,
mdiDevices,
mdiHeart,
mdiInformation,
mdiInformationOutline,
mdiLifebuoy,
@@ -321,6 +322,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconColor: "#301A8E",
component: "hassio",
},
{
path: "/config/system_health",
translationKey: "system_health",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
],
about: [
{
@@ -439,6 +447,10 @@ class HaPanelConfig extends HassRouterPage {
tag: "ha-config-section-storage",
load: () => import("./storage/ha-config-section-storage"),
},
system_health: {
tag: "ha-config-system-health",
load: () => import("./system-health/ha-config-system-health"),
},
updates: {
tag: "ha-config-section-updates",
load: () => import("./core/ha-config-section-updates"),

View File

@@ -14,8 +14,7 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
@@ -50,10 +49,6 @@ import {
fetchIntegrationManifests,
IntegrationManifest,
} from "../../../data/integration";
import {
getSupportedBrands,
getSupportedBrandsLookup,
} from "../../../data/supported_brands";
import { scanUSBDevices } from "../../../data/usb";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import {
@@ -682,84 +677,49 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (!domain) {
return;
}
const handlers = await getConfigFlowHandlers(this.hass, "integration");
// Integration exists, so we can just create a flow
if (handlers.includes(domain)) {
const localize = await localizePromise;
if (
!(await showConfirmationDialog(this, {
title: localize("ui.panel.config.integrations.confirm_new", {
integration: domainToName(localize, domain),
}),
}))
) {
if (!handlers.includes(domain)) {
if (HELPER_DOMAINS.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
});
}
const supportedBrands = await getSupportedBrands(this.hass);
const supportedBrandsIntegrations =
getSupportedBrandsLookup(supportedBrands);
// Supported brand exists, so we can just create a flow
if (Object.keys(supportedBrandsIntegrations).includes(domain)) {
const brand = supportedBrandsIntegrations[domain];
const slug = brand.supported_flows![0];
showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.supported_brand_flow",
{
supported_brand: brand.name,
flow_domain_name: domainToName(this.hass.localize, slug),
}
const helpers = await getConfigFlowHandlers(this.hass, "helper");
if (helpers.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
),
confirm: () => {
if (["zha", "zwave_js"].includes(slug)) {
protocolIntegrationPicked(this, this.hass, slug);
return;
}
fireEvent(this, "handler-picked", {
handler: slug,
});
},
});
return;
}
// If not an integration or supported brand, try helper else show alert
if (HELPER_DOMAINS.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
return;
}
const helpers = await getConfigFlowHandlers(this.hass, "helper");
if (helpers.includes(domain)) {
navigate(`/config/helpers/add?domain=${domain}`, {
replace: true,
});
const localize = await localizePromise;
if (
!(await showConfirmationDialog(this, {
title: localize("ui.panel.config.integrations.confirm_new", {
integration: domainToName(localize, domain),
}),
}))
) {
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
),
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
});
}

View File

@@ -7,7 +7,7 @@ import {
mdiDotsVertical,
mdiOpenInNew,
} from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item";
import "@polymer/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -64,15 +64,13 @@ export class HaIntegrationCard extends LitElement {
@property() public domain!: string;
@property({ attribute: false }) public items!: ConfigEntryExtended[];
@property() public items!: ConfigEntryExtended[];
@property({ attribute: false }) public manifest?: IntegrationManifest;
@property() public manifest?: IntegrationManifest;
@property({ attribute: false })
public entityRegistryEntries!: EntityRegistryEntry[];
@property() public entityRegistryEntries!: EntityRegistryEntry[];
@property({ attribute: false })
public deviceRegistryEntries!: DeviceRegistryEntry[];
@property() public deviceRegistryEntries!: DeviceRegistryEntry[];
@property() public selectedConfigEntryId?: string;
@@ -181,7 +179,7 @@ export class HaIntegrationCard extends LitElement {
const services = this._getServices(item, this.deviceRegistryEntries);
const entities = this._getEntities(item, this.entityRegistryEntries);
let stateText: Parameters<typeof this.hass.localize> | undefined;
let stateText: [string, ...unknown[]] | undefined;
let stateTextExtra: TemplateResult | string | undefined;
if (item.disabled_by) {
@@ -227,7 +225,7 @@ export class HaIntegrationCard extends LitElement {
for (const [items, localizeKey] of [
[devices, "devices"],
[services, "services"],
] as const) {
] as [DeviceRegistryEntry[], string][]) {
if (items.length === 0) {
continue;
}

View File

@@ -12,13 +12,13 @@ import { brandsUrl } from "../../../util/brands-url";
export class HaIntegrationHeader extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public banner?: string;
@property() public banner!: string;
@property() public localizedDomainName?: string;
@property() public domain!: string;
@property() public label?: string;
@property() public label!: string;
@property({ attribute: false }) public manifest?: IntegrationManifest;

View File

@@ -102,7 +102,7 @@ export class SystemLogCard extends LitElement {
<paper-item @click=${this._openLog} .logItem=${item}>
<paper-item-body two-line>
<div class="row">${item.message[0]}</div>
<div class="row-secondary" secondary>
<div secondary>
${this._timestamp(item)}
${html`(<span class=${item.level.toLowerCase()}
>${this.hass.localize(
@@ -209,11 +209,6 @@ export class SystemLogCard extends LitElement {
.empty-content {
direction: var(--direction);
}
.row-secondary {
direction: var(--direction);
text-align: left;
}
`;
}
}

View File

@@ -1,91 +0,0 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import type { NetworkInterface } from "../../../data/hassio/network";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { IPDetailDialogParams } from "./show-ip-detail-dialog";
@customElement("dialog-ip-detail")
class DialogIPDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: IPDetailDialogParams;
@state() private _interface?: NetworkInterface;
public showDialog(params: IPDetailDialogParams): void {
this._params = params;
this._interface = this._params.interface;
}
public closeDialog() {
this._params = undefined;
this._interface = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._interface) {
return html``;
}
const ipv4 = this._interface.ipv4;
const ipv6 = this._interface.ipv6;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(this.hass, "IP Information")}
>
${ipv4
? html`
<div>
<h3>IPv4</h3>
${ipv4.address
? html`<div>IP Address: ${ipv4.address?.join(", ")}</div>`
: ""}
${ipv4.gateway ? html`<div>Gateway: ${ipv4.gateway}</div>` : ""}
${ipv4.method ? html`<div>Method: ${ipv4.method}</div>` : ""}
${ipv4.nameservers?.length
? html`
<div>Name Servers: ${ipv4.nameservers?.join(", ")}</div>
`
: ""}
</div>
`
: ""}
${ipv6
? html`
<div>
<h3>IPv6</h3>
${ipv6.address
? html`<div>IP Address: ${ipv6.address?.join(", ")}</div>`
: ""}
${ipv6.gateway ? html`<div>Gateway: ${ipv6.gateway}</div>` : ""}
${ipv6.method ? html`<div>Method: ${ipv6.method}</div>` : ""}
${ipv6.nameservers?.length
? html`
<div>Name Servers: ${ipv6.nameservers?.join(", ")}</div>
`
: ""}
</div>
`
: ""}
</ha-dialog>
`;
}
static styles: CSSResultGroup = haStyleDialog;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ip-detail": DialogIPDetail;
}
}

View File

@@ -1,19 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { NetworkInterface } from "../../../data/hassio/network";
export interface IPDetailDialogParams {
interface?: NetworkInterface;
}
export const loadIPDetailDialog = () => import("./dialog-ip-detail");
export const showIPDetailDialog = (
element: HTMLElement,
dialogParams: IPDetailDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ip-detail",
dialogImport: loadIPDetailDialog,
dialogParams,
});
};

View File

@@ -1,16 +1,13 @@
import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab";
import "@material/mwc-tab-bar";
import { mdiDotsVertical } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
@@ -32,7 +29,7 @@ import {
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { showIPDetailDialog } from "./show-ip-detail-dialog";
import "../../../components/ha-card";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -239,25 +236,9 @@ export class HassioNetwork extends LitElement {
</ha-circular-progress>`
: this.hass.localize("ui.common.save")}
</mwc-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${"ui.common.menu"}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>IP Information</mwc-list-item>
</ha-button-menu>
</div>`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
showIPDetailDialog(this, { interface: this._interface });
break;
}
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;

View File

@@ -1,57 +0,0 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./integrations-startup-time";
@customElement("dialog-integration-startup")
class DialogIntegrationStartup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
public showDialog(): void {
this._opened = true;
}
public closeDialog() {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
)}
>
<integrations-startup-time
.hass=${this.hass}
narrow
></integrations-startup-time>
</ha-dialog>
`;
}
static styles: CSSResultGroup = haStyleDialog;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-integration-startup": DialogIntegrationStartup;
}
}

View File

@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-markdown";
import { ignoreRepairsIssue, RepairsIssue } from "../../../data/repairs";
import type { RepairsIssue } from "../../../data/repairs";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { RepairsIssueDialogParams } from "./show-repair-issue-dialog";
@@ -23,10 +23,6 @@ class DialogRepairsIssue extends LitElement {
}
public closeDialog() {
if (this._params?.dialogClosedCallback) {
this._params.dialogClosedCallback();
}
this._params = undefined;
this._issue = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -43,6 +39,7 @@ class DialogRepairsIssue extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.hideActions=${!this._issue.learn_more_url}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
@@ -53,24 +50,6 @@ class DialogRepairsIssue extends LitElement {
)}
>
<div>
<ha-alert
.alertType=${this._issue.severity === "error" ||
this._issue.severity === "critical"
? "error"
: "warning"}
.title=${this.hass.localize(
`ui.panel.config.repairs.${this._issue.severity}`
)}
>${this.hass.localize(
"ui.panel.config.repairs.dialog.alert_not_fixable"
)}
${this._issue.breaks_in_ha_version
? this.hass.localize(
"ui.panel.config.repairs.dialog.breaks_in_version",
{ version: this._issue.breaks_in_ha_version }
)
: ""}
</ha-alert>
<ha-markdown
allowsvg
breaks
@@ -81,14 +60,19 @@ class DialogRepairsIssue extends LitElement {
this._issue.translation_placeholders
)}
></ha-markdown>
${this._issue.breaks_in_ha_version
? html`
<br />This will no longer work as of the
${this._issue.breaks_in_ha_version} release of Home Assistant.
`
: ""}
<br />The issue is ${this._issue.severity} severity.<br />We can not
automatically repair this issue for you.
${this._issue.dismissed_version
? html`
<br /><span class="dismissed">
${this.hass.localize(
"ui.panel.config.repairs.dialog.ignored_in_version",
{ version: this._issue.dismissed_version }
)}</span
>
<br />This issue has been dismissed in version
${this._issue.dismissed_version}.
`
: ""}
</div>
@@ -100,43 +84,20 @@ class DialogRepairsIssue extends LitElement {
slot="primaryAction"
rel="noopener noreferrer"
>
<mwc-button
.label=${this.hass!.localize(
"ui.panel.config.repairs.dialog.learn"
)}
></mwc-button>
<mwc-button .label=${"Learn More"}></mwc-button>
</a>
`
: ""}
<mwc-button
slot="secondaryAction"
.label=${this._issue!.ignored
? this.hass!.localize("ui.panel.config.repairs.dialog.unignore")
: this.hass!.localize("ui.panel.config.repairs.dialog.ignore")}
@click=${this._ignoreIssue}
></mwc-button>
</ha-dialog>
`;
}
private _ignoreIssue() {
ignoreRepairsIssue(this.hass, this._issue!, !this._issue!.ignored);
this.closeDialog();
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-alert {
margin-bottom: 16px;
display: block;
}
a {
text-decoration: none;
}
.dismissed {
font-style: italic;
}
`,
];
}

View File

@@ -1,62 +1,26 @@
import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base";
import { mdiDotsVertical } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-card";
import {
RepairsIssue,
severitySort,
subscribeRepairsIssueRegistry,
} from "../../../data/repairs";
import type { RepairsIssue } from "../../../data/repairs";
import { fetchRepairsIssues } from "../../../data/repairs";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import "./ha-config-repairs";
import { showIntegrationStartupDialog } from "./show-integration-startup-dialog";
import { showSystemInformationDialog } from "./show-system-information-dialog";
@customElement("ha-config-repairs-dashboard")
class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
class HaConfigRepairsDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _repairsIssues: RepairsIssue[] = [];
@state() private _showIgnored = false;
private _getFilteredIssues = memoizeOne(
(showIgnored: boolean, repairsIssues: RepairsIssue[]) =>
showIgnored
? repairsIssues
: repairsIssues.filter((issue) => !issue.ignored)
);
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
this._repairsIssues = repairs.issues.sort(
(a, b) => severitySort[a.severity] - severitySort[b.severity]
);
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues) {
integrations.add(issue.domain);
}
this.hass.loadBackendTranslation("issues", [...integrations]);
}),
];
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._fetchIssues();
}
protected render(): TemplateResult {
const issues = this._getFilteredIssues(
this._showIgnored,
this._repairsIssues
);
return html`
<hass-subpage
back-path="/config/system"
@@ -64,39 +28,6 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
>
<div slot="toolbar-icon">
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${isComponentLoaded(this.hass, "system_health") ||
isComponentLoaded(this.hass, "hassio")
? html`
<mwc-list-item
@request-selected=${this._showSystemInformationDialog}
>
${this.hass.localize(
"ui.panel.config.repairs.system_information"
)}
</mwc-list-item>
`
: ""}
<mwc-list-item
@request-selected=${this._showIntegrationStartupDialog}
>
${this.hass.localize(
"ui.panel.config.repairs.integration_startup_time"
)}
</mwc-list-item>
<mwc-list-item @request-selected=${this._toggleIgnored}>
${this._showIgnored
? this.hass.localize("ui.panel.config.repairs.hide_ignored")
: this.hass.localize("ui.panel.config.repairs.show_ignored")}
</mwc-list-item>
</ha-button-menu>
</div>
<div class="content">
<ha-card outlined>
<div class="card-content">
@@ -105,7 +36,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
<ha-config-repairs
.hass=${this.hass}
.narrow=${this.narrow}
.repairsIssues=${issues}
.repairsIssues=${this._repairsIssues}
@update-issues=${this._fetchIssues}
></ha-config-repairs>
`
: html`
@@ -122,32 +54,13 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
`;
}
private _showSystemInformationDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
private async _fetchIssues(): Promise<void> {
this._repairsIssues = (await fetchRepairsIssues(this.hass)).issues;
const integrations: Set<string> = new Set();
for (const issue of this._repairsIssues) {
integrations.add(issue.domain);
}
showSystemInformationDialog(this);
}
private _showIntegrationStartupDialog(
ev: CustomEvent<RequestSelectedDetail>
): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
showIntegrationStartupDialog(this);
}
private _toggleIgnored(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
this._showIgnored = !this._showIgnored;
this.hass.loadBackendTranslation("issues", [...integrations]);
}
static styles = css`

View File

@@ -2,6 +2,7 @@ import "@material/mwc-list/mwc-list";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-list-item";
@@ -40,51 +41,43 @@ class HaConfigRepairs extends LitElement {
})}
</div>
<mwc-list>
${issues.map(
(issue) => html`
<ha-list-item
twoline
graphic="avatar"
.hasMeta=${!this.narrow}
.issue=${issue}
class=${issue.ignored ? "ignored" : ""}
@click=${this._openShowMoreDialog}
>
<img
loading="lazy"
src=${brandsUrl({
domain: issue.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
.title=${domainToName(this.hass.localize, issue.domain)}
referrerpolicy="no-referrer"
slot="graphic"
/>
<span
>${this.hass.localize(
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.title`
)}</span
>
<span slot="secondary" class="secondary">
${issue.created
? relativeTime(new Date(issue.created), this.hass.locale)
: ""}
${issue.ignored
? ` - ${this.hass.localize(
"ui.panel.config.repairs.dialog.ignored_in_version_short",
{ version: issue.dismissed_version }
)}`
: ""}
</span>
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""}
</ha-list-item>
`
${issues.map((issue) =>
issue.ignored
? ""
: html`
<ha-list-item
twoline
graphic="avatar"
.hasMeta=${!this.narrow}
.issue=${issue}
@click=${this._openShowMoreDialog}
>
<img
loading="lazy"
src=${brandsUrl({
domain: issue.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
.title=${domainToName(this.hass.localize, issue.domain)}
referrerpolicy="no-referrer"
slot="graphic"
/>
<span
>${this.hass.localize(
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.title`
)}</span
>
<span slot="secondary" class="secondary">
${issue.created
? relativeTime(new Date(issue.created), this.hass.locale)
: ""}
</span>
</ha-list-item>
`
)}
</mwc-list>
`;
@@ -93,11 +86,12 @@ class HaConfigRepairs extends LitElement {
private _openShowMoreDialog(ev): void {
const issue = ev.currentTarget.issue as RepairsIssue;
if (issue.is_fixable) {
showRepairsFlowDialog(this, issue);
} else {
showRepairsIssueDialog(this, {
issue,
showRepairsFlowDialog(this, issue, () => {
// @ts-ignore
fireEvent(this, "update-issues");
});
} else {
showRepairsIssueDialog(this, { issue: (ev.currentTarget as any).issue });
}
}
@@ -110,9 +104,6 @@ class HaConfigRepairs extends LitElement {
padding: 16px;
padding-bottom: 0;
}
.ignored {
opacity: var(--light-secondary-opacity);
}
button.show-more {
color: var(--primary-color);
text-align: left;

View File

@@ -1,12 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadIntegrationStartupDialog = () =>
import("./dialog-integration-startup");
export const showIntegrationStartupDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-integration-startup",
dialogImport: loadIntegrationStartupDialog,
dialogParams: {},
});
};

View File

@@ -3,7 +3,6 @@ import type { RepairsIssue } from "../../../data/repairs";
export interface RepairsIssueDialogParams {
issue: RepairsIssue;
dialogClosedCallback?: () => void;
}
export const loadRepairsIssueDialog = () => import("./dialog-repairs-issue");

View File

@@ -1,12 +0,0 @@
import { fireEvent } from "../../../common/dom/fire_event";
export const loadSystemInformationDialog = () =>
import("./dialog-system-information");
export const showSystemInformationDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-system-information",
dialogImport: loadSystemInformationDialog,
dialogParams: undefined,
});
};

View File

@@ -1,15 +1,17 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiContentCopy } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-circular-progress";
import "../../../components/ha-metric";
import { fetchHassioStats, HassioStats } from "../../../data/hassio/common";
import {
@@ -23,11 +25,12 @@ import {
SystemHealthInfo,
} from "../../../data/system_health";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../resources/styles";
import "../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "../../../components/ha-circular-progress";
import "./integrations-card";
const sortKeys = (a: string, b: string) => {
if (a === "homeassistant") {
@@ -50,40 +53,28 @@ export const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
};
@customElement("dialog-system-information")
class DialogSystemInformation extends LitElement {
@customElement("ha-config-system-health")
class HaConfigSystemHealth extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _systemInfo?: SystemHealthInfo;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _resolutionInfo?: HassioResolution;
@state() private _info?: SystemHealthInfo;
@state() private _supervisorStats?: HassioStats;
@state() private _resolutionInfo?: HassioResolution;
@state() private _coreStats?: HassioStats;
@state() private _opened = false;
@state() private _error?: { code: string; message: string };
private _subscriptions?: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>>;
public showDialog(): void {
this._opened = true;
this.hass!.loadBackendTranslation("system_health");
this._subscribe();
}
public closeDialog() {
this._opened = false;
this._unsubscribe();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _subscribe(): void {
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
const subs: Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> = [];
if (isComponentLoaded(this.hass, "system_health")) {
subs.push(
subscribeSystemHealthInfo(this.hass!, (info) => {
this._systemInfo = info;
this._info = info;
})
);
}
@@ -102,51 +93,149 @@ class DialogSystemInformation extends LitElement {
10000
)
);
fetchHassioResolution(this.hass).then((data) => {
this._resolutionInfo = data;
});
}
this._subscriptions = subs;
return subs;
}
private _unsubscribe() {
while (this._subscriptions?.length) {
const unsub = this._subscriptions.pop()!;
if (unsub instanceof Promise) {
unsub.then((unsubFunc) => unsubFunc());
} else {
unsub();
}
}
this._subscriptions = undefined;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._systemInfo = undefined;
this._resolutionInfo = undefined;
this._coreStats = undefined;
this._supervisorStats = undefined;
this.hass!.loadBackendTranslation("system_health");
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
const sections: TemplateResult[] = [];
if (!this._info) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._info).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._info[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_info"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
}
const sections = this._getSections();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.system_information")
)}
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/system"
.header=${this.hass.localize("ui.panel.config.system_health.caption")}
>
<div>
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._info
? html`
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._copyInfo}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.panel.config.info.copy_menu")}
.path=${mdiContentCopy}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_raw")}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize("ui.panel.config.info.copy_github")}
</mwc-list-item>
</ha-button-menu>
`
: ""}
<div class="content">
${this._resolutionInfo
? html`${this._resolutionInfo.unhealthy.length
? html`<ha-alert alert-type="error">
@@ -176,63 +265,66 @@ class DialogSystemInformation extends LitElement {
: ""} `
: ""}
<div>${sections}</div>
<ha-card outlined>
<div class="card-content">${sections}</div>
</ha-card>
${!this._coreStats && !this._supervisorStats
? ""
: html`
<div>
${this._coreStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.core_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._coreStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._coreStats.memory_percent}
></ha-metric>
`
: ""}
${this._supervisorStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.supervisor_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._supervisorStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._supervisorStats.memory_percent}
></ha-metric>
`
: ""}
</div>
<ha-card outlined>
<div class="card-content">
${this._coreStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.core_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._coreStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._coreStats.memory_percent}
></ha-metric>
`
: ""}
${this._supervisorStats
? html`
<h3>
${this.hass.localize(
"ui.panel.config.system_health.supervisor_stats"
)}
</h3>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.cpu_usage"
)}
.value=${this._supervisorStats.cpu_percent}
></ha-metric>
<ha-metric
.heading=${this.hass.localize(
"ui.panel.config.system_health.ram_usage"
)}
.value=${this._supervisorStats.memory_percent}
></ha-metric>
`
: ""}
</div>
</ha-card>
`}
<integrations-card
.hass=${this.hass}
.narrow=${this.narrow}
></integrations-card>
</div>
<mwc-button
slot="primaryAction"
.label=${this.hass.localize("ui.panel.config.repairs.copy")}
@click=${this._copyInfo}
></mwc-button>
</ha-dialog>
</hass-subpage>
`;
}
@@ -294,111 +386,17 @@ class DialogSystemInformation extends LitElement {
});
}
private _getSections(): TemplateResult[] {
const sections: TemplateResult[] = [];
if (!this._systemInfo) {
sections.push(
html`
<div class="loading-container">
<ha-circular-progress active></ha-circular-progress>
</div>
`
);
} else {
const domains = Object.keys(this._systemInfo).sort(sortKeys);
for (const domain of domains) {
const domainInfo = this._systemInfo[domain];
const keys: TemplateResult[] = [];
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (
domainInfo.info[key] &&
typeof domainInfo.info[key] === "object"
) {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
value = html`
<ha-circular-progress active size="tiny"></ha-circular-progress>
`;
} else if (info.type === "failed") {
value = html`
<span class="error">${info.error}</span>${!info.more_info
? ""
: html`
<a
href=${info.more_info}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
"ui.panel.config.info.system_health.more_systemInfo"
)}
</a>
`}
`;
} else if (info.type === "date") {
value = formatDateTime(new Date(info.value), this.hass.locale);
}
} else {
value = domainInfo.info[key];
}
keys.push(html`
<tr>
<td>
${this.hass.localize(
`component.${domain}.system_health.info.${key}`
) || key}
</td>
<td>${value}</td>
</tr>
`);
}
if (domain !== "homeassistant") {
sections.push(
html`
<div class="card-header">
<h3>${domainToName(this.hass.localize, domain)}</h3>
${!domainInfo.manage_url
? ""
: html`
<a class="manage" href=${domainInfo.manage_url}>
<mwc-button>
${this.hass.localize(
"ui.panel.config.info.system_health.manage"
)}
</mwc-button>
</a>
`}
</div>
`
);
}
sections.push(html`
<table>
${keys}
</table>
`);
}
}
return sections;
}
private async _copyInfo(): Promise<void> {
private async _copyInfo(ev: CustomEvent<ActionDetail>): Promise<void> {
const github = ev.detail.index === 1;
let haContent: string | undefined;
const domainParts: string[] = [];
for (const domain of Object.keys(this._systemInfo!).sort(sortKeys)) {
const domainInfo = this._systemInfo![domain];
for (const domain of Object.keys(this._info!).sort(sortKeys)) {
const domainInfo = this._info![domain];
let first = true;
const parts = [
`${
domain !== "homeassistant"
github && domain !== "homeassistant"
? `<details><summary>${domainToName(
this.hass.localize,
domain
@@ -410,7 +408,7 @@ class DialogSystemInformation extends LitElement {
for (const key of Object.keys(domainInfo.info)) {
let value: unknown;
if (domainInfo.info[key] && typeof domainInfo.info[key] === "object") {
if (typeof domainInfo.info[key] === "object") {
const info = domainInfo.info[key] as SystemCheckValueObject;
if (info.type === "pending") {
@@ -423,11 +421,11 @@ class DialogSystemInformation extends LitElement {
} else {
value = domainInfo.info[key];
}
if (first) {
if (github && first) {
parts.push(`${key} | ${value}\n-- | --`);
first = false;
} else {
parts.push(`${key} | ${value}`);
parts.push(`${key}${github ? " | " : ": "}${value}`);
}
}
@@ -435,14 +433,16 @@ class DialogSystemInformation extends LitElement {
haContent = parts.join("\n");
} else {
domainParts.push(parts.join("\n"));
if (domain !== "homeassistant") {
if (github && domain !== "homeassistant") {
domainParts.push("</details>");
}
}
}
await copyToClipboard(
`${"## "}System Information\n${haContent}\n\n${domainParts.join("\n\n")}`
`${github ? "## " : ""}System Health\n${haContent}\n\n${domainParts.join(
"\n\n"
)}`
);
showToast(this, {
@@ -450,50 +450,73 @@ class DialogSystemInformation extends LitElement {
});
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-alert {
margin-bottom: 16px;
display: block;
}
table {
width: 100%;
}
static styles: CSSResultGroup = css`
.content {
padding: 28px 20px 0;
max-width: 1040px;
margin: 0 auto;
}
integrations-card {
max-width: 600px;
display: block;
max-width: 600px;
margin: 0 auto;
margin-bottom: 24px;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
ha-card {
display: block;
max-width: 600px;
margin: 0 auto;
padding-bottom: 16px;
margin-bottom: 24px;
}
ha-alert {
display: block;
max-width: 500px;
margin: 0 auto;
margin-bottom: max(24px, env(safe-area-inset-bottom));
}
table {
width: 100%;
}
td:first-child {
width: 45%;
}
td:first-child {
width: 45%;
}
td:last-child {
direction: ltr;
}
td:last-child {
direction: ltr;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.card-header {
justify-content: space-between;
display: flex;
align-items: center;
}
.error {
color: var(--error-color);
}
.error {
color: var(--error-color);
}
a.manage {
text-decoration: none;
}
`,
];
a {
color: var(--primary-color);
}
a.manage {
text-decoration: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-system-information": DialogSystemInformation;
"ha-config-system-health": HaConfigSystemHealth;
}
}

View File

@@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("integrations-startup-time")
class IntegrationsStartupTime extends LitElement {
@customElement("integrations-card")
class IntegrationsCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@@ -45,47 +45,57 @@ class IntegrationsStartupTime extends LitElement {
}
return html`
<mwc-list>
${this._setups?.map((setup) => {
const manifest = this._manifests && this._manifests[setup.domain];
const docLink = manifest
? manifest.is_built_in
? documentationUrl(this.hass, `/integrations/${manifest.domain}`)
: manifest.documentation
: "";
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.system_health.integration_start_time"
)}
>
<mwc-list>
${this._setups?.map((setup) => {
const manifest = this._manifests && this._manifests[setup.domain];
const docLink = manifest
? manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation
: "";
const setupSeconds = setup.seconds?.toFixed(2);
return html`
<ha-clickable-list-item
graphic="avatar"
twoline
hasMeta
openNewTab
@click=${this._entryClicked}
href=${docLink}
>
<img
loading="lazy"
src=${brandsUrl({
domain: setup.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
slot="graphic"
/>
<span>
${domainToName(this.hass.localize, setup.domain, manifest)}
</span>
<span slot="secondary">${setup.domain}</span>
<div slot="meta">
${setupSeconds ? html`${setupSeconds} s` : ""}
</div>
</ha-clickable-list-item>
`;
})}
</mwc-list>
const setupSeconds = setup.seconds?.toFixed(2);
return html`
<ha-clickable-list-item
graphic="avatar"
twoline
hasMeta
openNewTab
@click=${this._entryClicked}
href=${docLink}
>
<img
loading="lazy"
src=${brandsUrl({
domain: setup.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
slot="graphic"
/>
<span>
${domainToName(this.hass.localize, setup.domain, manifest)}
</span>
<span slot="secondary">${setup.domain}</span>
<div slot="meta">
${setupSeconds ? html`${setupSeconds} s` : ""}
</div>
</ha-clickable-list-item>
`;
})}
</mwc-list>
</ha-card>
`;
}
@@ -139,6 +149,6 @@ class IntegrationsStartupTime extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"integrations-startup-time": IntegrationsStartupTime;
"integrations-card": IntegrationsCard;
}
}

View File

@@ -30,13 +30,13 @@ import { showUserDetailDialog } from "./show-dialog-user-detail";
export class HaConfigUsers extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public _users: User[] = [];
@property() public _users: User[] = [];
@property({ type: Boolean }) public isWide!: boolean;
@property() public isWide!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property() public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property() public route!: Route;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
@@ -76,8 +76,7 @@ export class HaConfigUsers extends LitElement {
width: "20%",
direction: "asc",
hidden: narrow,
template: (groupIds: User["group_ids"]) =>
html` ${localize(`groups.${groupIds[0]}`)} `,
template: (groupIds) => html` ${localize(`groups.${groupIds[0]}`)} `,
},
is_active: {
title: this.hass.localize(
@@ -239,9 +238,3 @@ export class HaConfigUsers extends LitElement {
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-users": HaConfigUsers;
}
}

View File

@@ -306,9 +306,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
border-radius: 100%;
color: var(--secondary-text-color);
z-index: 25;
inset-inline-start: initial;
inset-inline-end: 0;
direction: var(--direction);
}
.content {

View File

@@ -1,6 +1,6 @@
import "@material/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@@ -197,7 +197,14 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
}
static get styles(): CSSResultGroup {
return [haStyleDialog];
return [
haStyleDialog,
css`
ha-switch {
padding-bottom: 16px;
}
`,
];
}
}

View File

@@ -1218,7 +1218,7 @@
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to load updates",
"version_available": "Version {version_available} is available",
"more_updates": "Show all updates",
"more_updates": "+{count} updates",
"show": "show",
"show_skipped": "Show skipped",
"hide_skipped": "Hide skipped",
@@ -1231,25 +1231,11 @@
"description": "Find and fix issues with your Home Assistant instance",
"title": "{count} {count, plural,\n one {repair}\n other {repairs}\n}",
"no_repairs": "There are currently no repairs available",
"more_repairs": "Show all repairs",
"show_ignored": "Show ignored",
"hide_ignored": "Hide ignored",
"critical": "Critical",
"error": "Error",
"warning": "Warning",
"system_information": "System information",
"integration_startup_time": "Integration startup time",
"copy": "Copy",
"more_repairs": "+{count} repairs",
"dialog": {
"title": "Repair",
"fix": "Repair",
"learn": "Learn more",
"ignore": "Ignore",
"unignore": "Unignore",
"alert_not_fixable": "We can not repair this issue for you.",
"breaks_in_version": "This will break in version {version}. Please fix this issue before upgrading.",
"ignored_in_version_short": "Ignored in version {version}",
"ignored_in_version": "This issue was ignored in version {version}."
"learn": "Learn more"
}
},
"areas": {