Cleanup and new repository management for add-on store (#5750)

This commit is contained in:
Joakim Sørensen 2020-05-07 15:20:51 +02:00 committed by GitHub
parent 56754b4d43
commit 0961c9d05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 384 additions and 311 deletions

View File

@ -41,13 +41,19 @@ class HassioAddonRepositoryEl extends LitElement {
protected render(): TemplateResult {
const repo = this.repo;
const addons = this._getAddons(this.addons, this.filter);
let _addons = this.addons;
if (!this.hass.userData?.showAdvanced) {
_addons = _addons.filter((addon) => {
return !addon.advanced;
});
}
const addons = this._getAddons(_addons, this.filter);
if (this.filter && addons.length < 1) {
return html`
<div class="content">
<p class="description">
No results found in "${repo.name}"
No results found in "${repo.name}."
</p>
</div>
`;
@ -57,66 +63,55 @@ class HassioAddonRepositoryEl extends LitElement {
<h1>
${repo.name}
</h1>
<p class="description">
Maintained by ${repo.maintainer}<br />
<a class="repo" href=${repo.url} target="_blank" rel="noreferrer">
${repo.url}
</a>
</p>
<div class="card-group">
${addons.map(
(addon) => html`
${addon.advanced && !this.hass.userData?.showAdvanced
? ""
: html`
<paper-card
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed &&
addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle"}
.iconTitle=${addon.installed
? addon.installed !== addon.version
? "New version available"
: "Add-on is installed"
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "not_available"
: ""}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "unavailable"
: ""}
></hassio-card-content>
</div>
</paper-card>
`}
<paper-card
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed && addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle"}
.iconTitle=${addon.installed
? addon.installed !== addon.version
? "New version available"
: "Add-on is installed"
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "not_available"
: ""}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "unavailable"
: ""}
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>

View File

@ -12,15 +12,17 @@ import {
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import "../../../src/components/ha-icon";
import "../../../src/layouts/loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import "../components/hassio-search-input";
import "../../../src/common/search/search-input";
import "./hassio-addon-repository";
import "./hassio-repositories-editor";
import { supervisorTabs } from "../hassio-panel";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
return -1;
@ -76,7 +78,7 @@ class HassioAddonStore extends LitElement {
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
.filter=${this._filter}
.filter=${this._filter!}
></hassio-addon-repository>
`);
}
@ -92,28 +94,52 @@ class HassioAddonStore extends LitElement {
.tabs=${supervisorTabs}
>
<span slot="header">Add-on store</span>
<ha-icon-button
icon="hassio:reload"
<paper-menu-button
close-on-activate
no-animations
horizontal-align="right"
horizontal-offset="-5"
slot="toolbar-icon"
aria-label="Reload add-ons"
@click=${this.refreshData}
></ha-icon-button>
>
<ha-icon
icon="hassio:dots-vertical"
slot="dropdown-trigger"
alt="menu"
></ha-icon>
<paper-listbox slot="dropdown-content" role="listbox">
<paper-item @tap=${this._manageRepositories}>
Repositories
</paper-item>
<paper-item @tap=${this.refreshData}>
Reload
</paper-item>
</paper-listbox>
</paper-menu-button>
${repos.length === 0
? html`<loading-screen></loading-screen>`
: html`
<hassio-repositories-editor
.hass=${this.hass}
.repos=${this._repos!}
></hassio-repositories-editor>
<hassio-search-input
.filter=${this._filter}
@value-changed=${this._filterChanged}
></hassio-search-input>
<div class="search">
<search-input
no-label-float
no-underline
.filter=${this._filter}
@value-changed=${this._filterChanged}
></search-input>
</div>
${repos}
`}
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
Missing add-ons? Enable advanced mode on
<a href="/profile" target="_top">
your profile page
</a>
.
</div>
`
: ""}
</hass-tabs-subpage>
`;
}
@ -130,6 +156,13 @@ class HassioAddonStore extends LitElement {
}
}
private async _manageRepositories() {
showRepositoriesDialog(this, {
repos: this._repos!,
loadData: () => this._loadData(),
});
}
private async _loadData() {
try {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
@ -150,6 +183,25 @@ class HassioAddonStore extends LitElement {
hassio-addon-repository {
margin-top: 24px;
}
.search {
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
position: relative;
top: 2px;
}
.advanced {
padding: 12px;
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.advanced a {
margin-left: 0.5em;
color: var(--primary-color);
}
`;
}
}

View File

@ -1,152 +0,0 @@
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { repeat } from "lit-html/directives/repeat";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-call-api-button";
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/components/ha-icon";
@customElement("hassio-repositories-editor")
class HassioRepositoriesEditor extends LitElement {
@property() public hass!: HomeAssistant;
@property() public repos!: HassioAddonRepository[];
@property() private _repoUrl = "";
private _sortedRepos = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
protected render(): TemplateResult {
const repos = this._sortedRepos(this.repos);
return html`
<div class="content">
<h1>
Repositories
</h1>
<p class="description">
Configure which add-on repositories to fetch data from:
</p>
<div class="card-group">
${// Use repeat so that the fade-out from call-service-api-button
// stays with the correct repo after we add/delete one.
repeat(
repos,
(repo) => repo.slug,
(repo) => html`
<paper-card>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${repo.name}
.description=${repo.url}
icon="hassio:github-circle"
></hassio-card-content>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeRemoveRepoData(repos, repo.url)}
class="warning"
>
Remove
</ha-call-api-button>
</div>
</paper-card>
`
)}
<paper-card>
<div class="card-content add">
<ha-icon icon="hassio:github-circle"></ha-icon>
<paper-input
label="Add new repository by URL"
.value=${this._repoUrl}
@value-changed=${this._urlChanged}
></paper-input>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeAddRepoData(repos, this._repoUrl)}
>
Add
</ha-call-api-button>
</div>
</paper-card>
</div>
</div>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("repos")) {
this._repoUrl = "";
}
}
private _urlChanged(ev: PolymerChangedEvent<string>) {
this._repoUrl = ev.detail.value;
}
private computeRemoveRepoData(repoList, url) {
const list = repoList
.filter((repo) => repo.url !== url)
.map((repo) => repo.source);
return { addons_repositories: list };
}
private computeAddRepoData(repoList, url) {
const list = repoList ? repoList.map((repo) => repo.source) : [];
list.push(url);
return { addons_repositories: list };
}
static get styles(): CSSResultArray {
return [
hassioStyle,
css`
.add {
padding: 12px 16px;
}
ha-icon {
color: var(--secondary-text-color);
margin-right: 16px;
display: inline-block;
}
paper-input {
width: calc(100% - 49px);
display: inline-block;
margin-top: -4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-repositories-editor": HassioRepositoriesEditor;
}
}

View File

@ -1,81 +0,0 @@
import "@material/mwc-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-icon";
import "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
LitElement,
property,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-search-input")
class HassioSearchInput extends LitElement {
@property() private filter?: string;
protected render(): TemplateResult {
return html`
<div class="search-container">
<paper-input
label="Search"
.value=${this.filter}
@value-changed=${this._filterInputChanged}
>
<ha-icon icon="hassio:magnify" slot="prefix" class="prefix"></ha-icon>
${this.filter &&
html`
<ha-icon-button
slot="suffix"
class="suffix"
@click=${this._clearSearch}
icon="hassio:close"
alt="Clear"
title="Clear"
></ha-icon-button>
`}
</paper-input>
</div>
`;
}
private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) });
}
private async _filterInputChanged(e) {
this._filterChanged(e.target.value);
}
private async _clearSearch() {
this._filterChanged("");
}
static get styles(): CSSResult {
return css`
paper-input {
flex: 1 1 auto;
margin: 0 16px;
}
.search-container {
display: inline-flex;
width: 100%;
align-items: center;
}
.prefix {
margin: 8px;
}
ha-icon {
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-search-input": HassioSearchInput;
}
}

View File

@ -0,0 +1,232 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-spinner/paper-spinner";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import {
HassioAddonRepository,
fetchHassioAddonsInfo,
} from "../../../../src/data/hassio/addon";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input") private _optionInput?: PaperInputElement;
@property() private _opened = false;
@property() private _prosessing = false;
@property() private _error?: string;
public async showDialog(_dialogParams: any): Promise<void> {
this._dialogParams = _dialogParams;
this._repos = _dialogParams.repos;
this._opened = true;
await this.updateComplete;
}
public closeDialog(): void {
this._opened = false;
this._error = "";
}
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
protected render(): TemplateResult {
const repositories = this._filteredRepositories(this._repos);
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
heading="Manage add-on repositories"
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="form">
${repositories.length
? repositories.map((repo) => {
return html`
<paper-item class="option">
<paper-item-body three-line>
<div>${repo.name}</div>
<div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div>
</paper-item-body>
<ha-icon
.slug=${repo.slug}
title="Remove"
@click=${this._removeRepository}
icon="hassio:delete"
></ha-icon>
</paper-item>
`;
})
: html`
<paper-item>
No repositories
</paper-item>
`}
<div class="layout horizontal bottom">
<paper-input
class="flex-auto"
id="repository_input"
label="Add repository"
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<paper-spinner active></paper-spinner>`
: "Add"}
</mwc-button>
</div>
</div>
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
Close
</mwc-button>
</ha-dialog>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
public focus() {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
private _handleKeyAdd(ev: KeyboardEvent) {
ev.stopPropagation();
if (ev.keyCode !== 13) {
return;
}
this._addRepository();
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._prosessing = true;
const repositories = this._filteredRepositories(this._repos);
const newRepositories = repositories.map((repo) => {
return repo.source;
});
newRepositories.push(input.value);
try {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
input.value = "";
} catch (err) {
this._error = err.message;
}
this._prosessing = false;
}
private async _removeRepository(ev: Event) {
const slug = (ev.target as any).slug;
const repositories = this._filteredRepositories(this._repos);
const repository = repositories.find((repo) => {
return repo.slug === slug;
});
if (!repository) {
return;
}
const newRepositories = repositories
.map((repo) => {
return repo.source;
})
.filter((repo) => {
return repo !== repository.source;
});
try {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
} catch (err) {
this._error = err.message;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-repositories": HassioRepositoriesDialog;
}
}

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-repositories";
import { HassioAddonRepository } from "../../../../src/data/hassio/addon";
export interface HassioRepositoryDialogParams {
repos: HassioAddonRepository[];
loadData: () => Promise<void>;
}
export const showRepositoriesDialog = (
element: HTMLElement,
dialogParams: HassioRepositoryDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-repositories",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories"
),
dialogParams,
});
};

View File

@ -77,6 +77,10 @@ class SearchInput extends LitElement {
static get styles(): CSSResult {
return css`
ha-icon,
ha-icon-button {
color: var(--primary-text-color);
}
ha-icon {
margin: 8px;
}

View File

@ -16,7 +16,8 @@ export interface CreateSessionResponse {
}
export interface SupervisorOptions {
channel: "beta" | "dev" | "stable";
channel?: "beta" | "dev" | "stable";
addons_repositories?: string[];
}
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {