Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a new version/pre-release selector #762

Merged
merged 2 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 170 additions & 31 deletions src/components/dialogs/hacs-download-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import { mainWindow } from "../../../homeassistant-frontend/src/common/dom/get_m
import { computeRTL } from "../../../homeassistant-frontend/src/common/util/compute_rtl";
import "../../../homeassistant-frontend/src/components/ha-alert";
import "../../../homeassistant-frontend/src/components/ha-button";
import "../../../homeassistant-frontend/src/components/ha-circular-progress";
import "../../../homeassistant-frontend/src/components/ha-dialog";
import "../../../homeassistant-frontend/src/components/ha-expansion-panel";
import "../../../homeassistant-frontend/src/components/ha-form/ha-form";
import "../../../homeassistant-frontend/src/components/ha-list-item";

import { relativeTime } from "../../../homeassistant-frontend/src/common/datetime/relative_time";
import { showConfirmationDialog } from "../../../homeassistant-frontend/src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../homeassistant-frontend/src/types";
import { HacsDispatchEvent } from "../../data/common";
Expand All @@ -18,12 +23,61 @@ import {
RepositoryBase,
repositoryDownloadVersion,
RepositoryInfo,
repositoryReleases,
} from "../../data/repository";
import { websocketSubscription } from "../../data/websocket";
import { HacsStyles } from "../../styles/hacs-common-style";
import { generateFrontendResourceURL } from "../../tools/frontend-resource";
import type { HacsDownloadDialogParams } from "./show-hacs-dialog";

@customElement("release-item")
export class ReleaseItem extends LitElement {
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
@property({ attribute: false }) public release!: {
tag: string;
published_at: string;
name: string;
prerelease: boolean;
};

protected render() {
return html`
<span>
${this.release.tag}
${this.release.prerelease ? html`<span class="pre-release">pre-release</span>` : nothing}
</span>
<span class="secondary">
${relativeTime(new Date(this.release.published_at), this.locale)}
${this.release.name && this.release.name !== this.release.tag
? html` - ${this.release.name}`
: nothing}
</span>
`;
}

static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
flex-direction: column;
}
.secondary {
font-size: 0.8em;
color: var(--secondary-text-color);
font-style: italic;
}
.pre-release {
background-color: var(--accent-color);
padding: 2px 4px;
font-size: 0.8em;
font-weight: 600;
border-radius: 12px;
margin: 0 2px;
color: var(--secondary-background-color);
}
`;
}
}
@customElement("hacs-download-dialog")
export class HacsDonwloadDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
Expand All @@ -34,6 +88,13 @@ export class HacsDonwloadDialog extends LitElement {

@state() private _error?: any;

@state() private _releases?: {
tag: string;
name: string;
published_at: string;
prerelease: boolean;
}[];

@state() public _repository?: RepositoryInfo;

@state() _dialogParams?: HacsDownloadDialogParams;
Expand All @@ -52,6 +113,7 @@ export class HacsDonwloadDialog extends LitElement {
if (this._repository && this._repository.version_or_commit !== "commit") {
this._selectedVersion = this._repository.available_version;
}
this._releases = undefined;

websocketSubscription(
this.hass,
Expand All @@ -70,6 +132,8 @@ export class HacsDonwloadDialog extends LitElement {
this._error = undefined;
this._installing = false;
this._waiting = false;
this._releases = undefined;
this._selectedVersion = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}

Expand All @@ -82,16 +146,35 @@ export class HacsDonwloadDialog extends LitElement {
});

private async _fetchRepository() {
this._repository = await fetchRepositoryInformation(
this.hass,
this._dialogParams!.repositoryId,
);
try {
this._repository = await fetchRepositoryInformation(
this.hass,
this._dialogParams!.repositoryId,
);
} catch (err: any) {
this._error = err;
}
Comment on lines +149 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address forbidden non-null assertion.

Replace the non-null assertion with the optional chain operator for safer runtime checks.

-      this._repository = await fetchRepositoryInformation(
-        this.hass,
-        this._dialogParams!.repositoryId,
-      );
+      this._repository = await fetchRepositoryInformation(
+        this.hass,
+        this._dialogParams?.repositoryId,
+      );
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
this._repository = await fetchRepositoryInformation(
this.hass,
this._dialogParams!.repositoryId,
);
} catch (err: any) {
this._error = err;
}
try {
this._repository = await fetchRepositoryInformation(
this.hass,
this._dialogParams?.repositoryId,
);
} catch (err: any) {
this._error = err;
}
Tools
Biome

[error] 152-152: Forbidden non-null assertion.

Unsafe fix: Replace with optional chain operator ?. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator

(lint/style/noNonNullAssertion)

}

protected render() {
if (!this._dialogParams || !this._repository) {
if (!this._dialogParams) {
return nothing;
}
if (!this._repository) {
return html`
<ha-dialog open scrimClickAction escapeKeyAction heading="Loading...">
<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
${this._error
? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}>
${this._error.message || this._error}
</ha-alert>`
: nothing}
</div>
</ha-dialog>
`;
}

const installPath = this._getInstallPath(this._repository);
return html`
<ha-dialog
Expand All @@ -102,14 +185,18 @@ export class HacsDonwloadDialog extends LitElement {
@closed=${this.closeDialog}
>
<div class="content">
${!this._repository.can_download
? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}>
${this._dialogParams.hacs.localize("confirm.home_assistant_version_not_correct", {
haversion: this.hass.config.version,
minversion: this._repository.homeassistant,
})}
</ha-alert>`
: ""}
<p>
${this._dialogParams.hacs.localize(
this._repository.version_or_commit === "commit"
? "dialog_download.will_download_commit"
: "dialog_download.will_download_version",
{
ref: html`
<code>${this._selectedVersion || this._repository.available_version}</code>
`,
},
)}
</p>
<div class="note">
${this._dialogParams.hacs.localize("dialog_download.note_downloaded", {
location: html`<code>'${installPath}'</code>`,
Expand All @@ -118,7 +205,7 @@ export class HacsDonwloadDialog extends LitElement {
this._dialogParams.hacs.info.lovelace_mode !== "storage"
? html`
<p>${this._dialogParams.hacs.localize(`dialog_download.lovelace_instruction`)}</p>
<pre>
<pre class="frontend-resource">
url: ${generateFrontendResourceURL({ repository: this._repository })}
type: module
</pre
Expand All @@ -129,9 +216,44 @@ export class HacsDonwloadDialog extends LitElement {
? html`<p>${this._dialogParams.hacs.localize("dialog_download.restart")}</p>`
: nothing}
</div>
${this._error?.message
${this._selectedVersion
? html`<ha-expansion-panel
@expanded-changed=${this._fetchReleases}
.header=${this._dialogParams.hacs.localize(`dialog_download.different_version`)}
>
Comment on lines +221 to +223
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address forbidden non-null assertion.

Replace the non-null assertion with the optional chain operator for safer runtime checks.

-                .header=${this._dialogParams.hacs.localize(`dialog_download.different_version`)}
+                .header=${this._dialogParams?.hacs.localize(`dialog_download.different_version`)}

Committable suggestion was skipped due to low confidence.

<p>${this._dialogParams!.hacs.localize("dialog_download.release_warning")}</p>
Comment on lines +221 to +224
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address forbidden non-null assertion.

Replace the non-null assertion with the optional chain operator for safer runtime checks.

-                .header=${this._dialogParams.hacs.localize(`dialog_download.different_version`)}
+                .header=${this._dialogParams?.hacs.localize(`dialog_download.different_version`)}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@expanded-changed=${this._fetchReleases}
.header=${this._dialogParams.hacs.localize(`dialog_download.different_version`)}
>
<p>${this._dialogParams!.hacs.localize("dialog_download.release_warning")}</p>
@expanded-changed=${this._fetchReleases}
.header=${this._dialogParams?.hacs.localize(`dialog_download.different_version`)}
>
<p>${this._dialogParams!.hacs.localize("dialog_download.release_warning")}</p>
Tools
Biome

[error] 224-224: Forbidden non-null assertion.

Unsafe fix: Replace with optional chain operator ?. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator

(lint/style/noNonNullAssertion)

${this._releases === undefined
? this._dialogParams.hacs.localize("dialog_download.fetching_releases")
: this._releases.length === 0
? this._dialogParams.hacs.localize("dialog_download.no_releases")
: html`<ha-form
@value-changed=${this._versionChanged}
.computeLabel=${this._computeLabel}
.schema=${[
{
name: "release",
selector: {
select: {
mode: "dropdown",
options: this._releases?.map((release) => ({
value: release.tag,
label: html`<release-item
.locale=${this.hass.locale}
.release=${release}
>
${release.tag}
</release-item>`,
})),
},
},
},
]}
></ha-form>`}
</ha-expansion-panel>`
: nothing}
${this._error
? html`<ha-alert alert-type="error" .rtl=${computeRTL(this.hass)}>
${this._error.message}
${this._error.message || this._error}
</ha-alert>`
: nothing}
${this._installing
Expand All @@ -143,7 +265,7 @@ export class HacsDonwloadDialog extends LitElement {
</mwc-button>
<mwc-button
slot="primaryAction"
?disabled=${!this._repository.can_download || this._waiting || this._installing}
?disabled=${this._waiting || this._installing}
@click=${this._installRepository}
>
${this._dialogParams.hacs.localize("common.download")}
Expand All @@ -152,6 +274,11 @@ export class HacsDonwloadDialog extends LitElement {
`;
}

private _computeLabel = (entry: any): string =>
entry.name === "release"
? this._dialogParams!.hacs.localize("dialog_download.release")
Comment on lines +277 to +279
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Address forbidden non-null assertion.

Replace the non-null assertion with the optional chain operator for safer runtime checks.

-      ? this._dialogParams!.hacs.localize("dialog_download.release")
+      ? this._dialogParams?.hacs.localize("dialog_download.release")
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private _computeLabel = (entry: any): string =>
entry.name === "release"
? this._dialogParams!.hacs.localize("dialog_download.release")
private _computeLabel = (entry: any): string =>
entry.name === "release"
? this._dialogParams?.hacs.localize("dialog_download.release")
Tools
Biome

[error] 279-279: Forbidden non-null assertion.

Unsafe fix: Replace with optional chain operator ?. This operator includes runtime checks, so it is safer than the compile-only non-null assertion operator

(lint/style/noNonNullAssertion)

: entry.name;

private async _installRepository(): Promise<void> {
if (!this._repository) {
return;
Expand All @@ -167,21 +294,14 @@ export class HacsDonwloadDialog extends LitElement {
return;
}

if (!this._repository.can_download) {
this._error = "Can not download this repository.";
return;
}

this._installing = true;
this._error = undefined;

try {
await repositoryDownloadVersion(
this.hass,
String(this._repository.id),
this._repository?.version_or_commit !== "commit"
? this._repository.available_version
: undefined,
this._selectedVersion || this._repository.available_version,
);
} catch (err: any) {
this._error = err || {
Expand Down Expand Up @@ -216,27 +336,45 @@ export class HacsDonwloadDialog extends LitElement {
}
}

async _fetchReleases() {
if (this._releases !== undefined) {
return;
}
try {
this._releases = await repositoryReleases(this.hass, this._repository!.id);
} catch (error) {
this._error = error;
}
}

private _versionChanged(ev: CustomEvent) {
this._selectedVersion = ev.detail.value.release;
}

static get styles(): CSSResultGroup {
return [
HacsStyles,
css`
.note {
margin-top: 12px;
}
.lovelace {
margin-top: 8px;
}
.learn_more {
color: var(--hcv-text-color-primary);
}
pre {
white-space: pre-line;
user-select: all;
padding: 8px;
}
mwc-linear-progress {
margin-bottom: -8px;
margin-top: 4px;
}
ha-expansion-panel {
background-color: var(--secondary-background-color);
padding: 8px;
}
.loading {
text-align: center;
padding: 16px;
}
`,
];
}
Expand All @@ -245,5 +383,6 @@ export class HacsDonwloadDialog extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hacs-download-dialog": HacsDonwloadDialog;
"release-item": ReleaseItem;
}
}
8 changes: 8 additions & 0 deletions src/data/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ export const repositoryDownloadVersion = async (
repository: repository,
version,
});

export const repositoryReleases = async (hass: HomeAssistant, repositoryId: string) =>
hass.connection.sendMessagePromise<
{ tag: string; name: string; published_at: string; prerelease: boolean }[]
>({
type: "hacs/repository/releases",
repository_id: repositoryId,
});
Comment on lines +72 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Add error handling for repositoryReleases function call.

Currently, the repositoryReleases function call within the _fetchReleases method lacks error handling. To ensure robustness, wrap the await call in a try-catch block to handle potential errors gracefully.

  • File: src/components/dialogs/hacs-download-dialog.ts
  • Lines: Around the _fetchReleases method

Example:

async _fetchReleases() {
  if (this._releases !== undefined) {
    return;
  }
  try {
    this._releases = await repositoryReleases(this.hass, this._repository!.id);
  } catch (error) {
    console.error("Failed to fetch releases:", error);
    this._error = error;
  }
}

Remember, even code needs a safety net sometimes! 🐰✨

Analysis chain

Verify error handling and function usage.

The function repositoryReleases looks good, but ensure that it handles errors gracefully and that all function calls match the expected response structure.


Alright, it looks like our previous search came up empty. Let's widen our net and see if we can catch any fish this time. We'll search through both JavaScript and TypeScript files and grab a bit more context around each instance of repositoryReleases. Here we go!


Alright, it looks like we found where repositoryReleases is hanging out! Now, let's zoom in on hacs-download-dialog.ts and see if it's handling errors like a pro or just winging it. Time to get our detective hats on and dig deeper!

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify all function calls to `repositoryReleases` handle errors and match the expected response structure.

# Test: Search for the function usage. Expect: Proper error handling and matching response structure.
rg --type js -A 5 $'repositoryReleases'

Length of output: 38


Script:

#!/bin/bash
# Description: Verify all function calls to `repositoryReleases` handle errors and match the expected response structure.

# Test: Search for the function usage in both JavaScript and TypeScript files. Expect: Proper error handling and matching response structure.
rg --type-add 'ts:*.{ts,tsx}' --type-add 'js:*.{js,jsx}' -tjs -tts -A 10 'repositoryReleases'

Length of output: 2402


Script:

#!/bin/bash
# Description: Extract more lines around the usage of `repositoryReleases` in `hacs-download-dialog.ts` to verify error handling.

# Test: Extract 20 lines before and after the usage of `repositoryReleases` in `hacs-download-dialog.ts`.
rg --type ts -A 20 -B 20 'repositoryReleases' src/components/dialogs/hacs-download-dialog.ts

Length of output: 3207

9 changes: 8 additions & 1 deletion src/localize/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
"dialog_download": {
"type": "Type",
"url": "URL",
"will_download_version": "Version {ref} will be downloaded",
"will_download_commit": "Commit {ref} will be downloaded",
"different_version": "Need a different version?",
"fetching_releases": "Fetching releases...",
"no_releases": "No releases found",
"release": "Release",
"release_warning": "It is not advised to use this section to do a rollback, restore a backup instead.",
"restart": "Remember that you need to restart Home Assistant before changes to integrations (custom_components) are applied.",
"selector_note": "The version selector will be removed in a future release. If you need to install a specific version, you can do so by using the service call for update entities.",
"note_downloaded": "When downloaded, this will be located in {location}",
Expand Down Expand Up @@ -165,4 +172,4 @@
}
}
}
}
}