Compare commits

...

2 Commits

Author SHA1 Message Date
Paulus Schoutsen 40bfd1b70a Move uninstall into overflow 2026-03-30 11:30:41 -04:00
Paulus Schoutsen 91670c8412 Move app actions around 2026-03-29 11:28:32 -04:00
@@ -3,9 +3,12 @@ import {
mdiChip, mdiChip,
mdiCircleOffOutline, mdiCircleOffOutline,
mdiCursorDefaultClickOutline, mdiCursorDefaultClickOutline,
mdiDelete,
mdiDocker, mdiDocker,
mdiDotsVertical,
mdiExclamationThick, mdiExclamationThick,
mdiFlask, mdiFlask,
mdiHammer,
mdiKey, mdiKey,
mdiLinkLock, mdiLinkLock,
mdiNetwork, mdiNetwork,
@@ -19,7 +22,9 @@ import {
mdiNumeric8, mdiNumeric8,
mdiPlayCircle, mdiPlayCircle,
mdiPound, mdiPound,
mdiRestart,
mdiShield, mdiShield,
mdiStop,
} from "@mdi/js"; } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -37,7 +42,10 @@ import "../../../../../components/chips/ha-chip-set";
import "../../../../../components/ha-alert"; import "../../../../../components/ha-alert";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
import "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-formfield"; import "../../../../../components/ha-formfield";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-markdown"; import "../../../../../components/ha-markdown";
import "../../../../../components/ha-settings-row"; import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
@@ -119,15 +127,16 @@ class SupervisorAppInfo extends LitElement {
@state() private _error?: string; @state() private _error?: string;
private _fetchDataTimeout?: number; private _pollInterval?: number;
public connectedCallback() {
super.connectedCallback();
this._startPolling();
}
public disconnectedCallback() { public disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this._stopPolling();
if (this._fetchDataTimeout) {
clearTimeout(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -223,6 +232,74 @@ class SupervisorAppInfo extends LitElement {
.path=${mdiCircleOffOutline} .path=${mdiCircleOffOutline}
></ha-svg-icon> ></ha-svg-icon>
`} `}
<ha-dropdown>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
.label=${this.hass.localize("ui.common.overflow_menu")}
></ha-icon-button>
${this._computeIsRunning
? html`
<ha-dropdown-item
@click=${this._stopClicked}
.disabled=${this._isSystemManaged(this.addon) &&
!this.controlEnabled}
variant="danger"
>
<ha-svg-icon
slot="icon"
.path=${mdiStop}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dashboard.stop"
)}
</ha-dropdown-item>
<ha-dropdown-item
@click=${this._restartClicked}
.disabled=${this._isSystemManaged(this.addon) &&
!this.controlEnabled}
variant="danger"
>
<ha-svg-icon
slot="icon"
.path=${mdiRestart}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dashboard.restart"
)}
</ha-dropdown-item>
`
: nothing}
${this.addon.build
? html`
<ha-dropdown-item
@click=${this._rebuildClicked}
variant="danger"
>
<ha-svg-icon
slot="icon"
.path=${mdiHammer}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dashboard.rebuild"
)}
</ha-dropdown-item>
`
: nothing}
<ha-dropdown-item
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
variant="danger"
>
<ha-svg-icon
slot="icon"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall"
)}
</ha-dropdown-item>
</ha-dropdown>
` `
: html` ${this.addon.version_latest} `} : html` ${this.addon.version_latest} `}
</div> </div>
@@ -654,95 +731,46 @@ class SupervisorAppInfo extends LitElement {
: nothing} : nothing}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<div></div>
<div> <div>
${this.addon.version ${this.addon.version
? this._computeIsRunning ? this._computeIsRunning
? html` ? html`
<ha-progress-button ${this._computeShowWebUI || this._computeShowIngressUI
variant="danger" ? html`
appearance="plain" <ha-button
@click=${this._stopClicked} href=${ifDefined(
.disabled=${systemManaged && !this.controlEnabled} !this._computeShowIngressUI
> ? this._pathWebui!
${this.hass.localize( : nothing
"ui.panel.config.apps.dashboard.stop" )}
)} target=${ifDefined(
</ha-progress-button> !this._computeShowIngressUI ? "_blank" : nothing
<ha-progress-button )}
variant="danger" rel=${ifDefined(
appearance="plain" !this._computeShowIngressUI ? "noopener" : nothing
@click=${this._restartClicked} )}
.disabled=${systemManaged && !this.controlEnabled} @click=${!this._computeShowWebUI
> ? this._openIngress
${this.hass.localize( : undefined}
"ui.panel.config.apps.dashboard.restart" >
)} ${this.hass.localize(
</ha-progress-button> "ui.panel.config.apps.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
` `
: html` : html`
<ha-progress-button <ha-progress-button
@click=${this._startClicked} @click=${this._startClicked}
.progress=${this.addon.state === "startup"} .progress=${this.addon.state === "startup"}
appearance="plain"
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.apps.dashboard.start" "ui.panel.config.apps.dashboard.start"
)} )}
</ha-progress-button> </ha-progress-button>
` `
: nothing}
</div>
<div>
${this.addon.version
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall"
)}
</ha-progress-button>
${this.addon.build
? html`
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._rebuildClicked}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.rebuild"
)}
</ha-progress-button>
`
: nothing}
${this._computeShowWebUI || this._computeShowIngressUI
? html`
<ha-button
href=${ifDefined(
!this._computeShowIngressUI
? this._pathWebui!
: nothing
)}
target=${ifDefined(
!this._computeShowIngressUI ? "_blank" : nothing
)}
rel=${ifDefined(
!this._computeShowIngressUI ? "noopener" : nothing
)}
@click=${!this._computeShowWebUI
? this._openIngress
: undefined}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.open_web_ui"
)}
</ha-button>
`
: nothing}
`
: html` : html`
<ha-progress-button <ha-progress-button
.disabled=${!this.addon.available} .disabled=${!this.addon.available}
@@ -775,38 +803,39 @@ class SupervisorAppInfo extends LitElement {
protected updated(changedProps) { protected updated(changedProps) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("addon")) { if (changedProps.has("addon")) {
this._loadData(); this._loadMetrics();
if (
!this._fetchDataTimeout &&
this.addon &&
"state" in this.addon &&
this.addon.state === "startup"
) {
// App is starting up, wait for it to start
this._scheduleDataUpdate();
}
} }
} }
private _scheduleDataUpdate() { private _startPolling() {
this._fetchDataTimeout = window.setTimeout(async () => { if (this._pollInterval) {
const addon = await fetchHassioAddonInfo(this.hass, this.addon.slug); return;
if (addon.state !== "startup") { }
this._fetchDataTimeout = undefined; this._pollInterval = window.setInterval(() => {
this.addon = addon; this._refreshAddonInfo();
const eventdata = { }, 5000);
success: true,
response: undefined,
path: "start",
};
fireEvent(this, "hass-api-called", eventdata);
} else {
this._scheduleDataUpdate();
}
}, 500);
} }
private async _loadData(): Promise<void> { private _stopPolling() {
if (this._pollInterval) {
clearInterval(this._pollInterval);
this._pollInterval = undefined;
}
}
private async _refreshAddonInfo(): Promise<void> {
if (!this.addon?.slug) {
return;
}
try {
const addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
this.addon = addon;
} catch {
// Ignore errors during polling
}
}
private async _loadMetrics(): Promise<void> {
if ("state" in this.addon && this.addon.state === "started") { if ("state" in this.addon && this.addon.state === "started") {
this._metrics = await fetchHassioStats( this._metrics = await fetchHassioStats(
this.hass, this.hass,
@@ -1069,14 +1098,11 @@ class SupervisorAppInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _stopClicked(ev: CustomEvent): Promise<void> { private async _stopClicked(): Promise<void> {
if (this._isSystemManaged(this.addon) && !this.controlEnabled) { if (this._isSystemManaged(this.addon) && !this.controlEnabled) {
return; return;
} }
const button = ev.currentTarget as any;
button.progress = true;
try { try {
await stopHassioAddon(this.hass, this.addon.slug); await stopHassioAddon(this.hass, this.addon.slug);
const eventdata = { const eventdata = {
@@ -1093,17 +1119,13 @@ class SupervisorAppInfo extends LitElement {
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} }
button.progress = false;
} }
private async _restartClicked(ev: CustomEvent): Promise<void> { private async _restartClicked(): Promise<void> {
if (this._isSystemManaged(this.addon) && !this.controlEnabled) { if (this._isSystemManaged(this.addon) && !this.controlEnabled) {
return; return;
} }
const button = ev.currentTarget as any;
button.progress = true;
try { try {
await restartHassioAddon(this.hass, this.addon.slug); await restartHassioAddon(this.hass, this.addon.slug);
const eventdata = { const eventdata = {
@@ -1120,13 +1142,9 @@ class SupervisorAppInfo extends LitElement {
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} }
button.progress = false;
} }
private async _rebuildClicked(ev: CustomEvent): Promise<void> { private async _rebuildClicked(): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try { try {
await rebuildLocalAddon(this.hass, this.addon.slug); await rebuildLocalAddon(this.hass, this.addon.slug);
} catch (err: any) { } catch (err: any) {
@@ -1137,7 +1155,6 @@ class SupervisorAppInfo extends LitElement {
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} }
button.progress = false;
} }
private async _startClicked(ev: CustomEvent): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
@@ -1204,13 +1221,11 @@ class SupervisorAppInfo extends LitElement {
navigate(`/config/app/${this.addon.slug}/config`); navigate(`/config/app/${this.addon.slug}/config`);
} }
private async _uninstallClicked(ev: CustomEvent): Promise<void> { private async _uninstallClicked(): Promise<void> {
if (this._isSystemManaged(this.addon) && !this.controlEnabled) { if (this._isSystemManaged(this.addon) && !this.controlEnabled) {
return; return;
} }
const button = ev.currentTarget as any;
button.progress = true;
let removeData = false; let removeData = false;
const _removeDataToggled = (e: Event) => { const _removeDataToggled = (e: Event) => {
removeData = (e.target as HaSwitch).checked; removeData = (e.target as HaSwitch).checked;
@@ -1246,7 +1261,6 @@ class SupervisorAppInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
@@ -1259,7 +1273,6 @@ class SupervisorAppInfo extends LitElement {
path: "uninstall", path: "uninstall",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
button.actionSuccess();
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@@ -1267,9 +1280,7 @@ class SupervisorAppInfo extends LitElement {
), ),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
button.actionError();
} }
button.progress = false;
} }
private _isSystemManaged = memoizeOne( private _isSystemManaged = memoizeOne(
@@ -1318,7 +1329,8 @@ class SupervisorAppInfo extends LitElement {
.addon-version { .addon-version {
float: var(--float-end); float: var(--float-end);
font-size: var(--ha-font-size-l); font-size: var(--ha-font-size-l);
vertical-align: middle; display: flex;
align-items: center;
} }
.errors { .errors {
color: var(--error-color); color: var(--error-color);