mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-20 08:27:59 +00:00
Compare commits
3 Commits
configurab
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c16c78984 | ||
|
|
767d43021f | ||
|
|
fbeab4e151 |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
<!--
|
||||
If your issue is about how an entity is shown in the UI, please add the state
|
||||
and attributes for all situations with a screenshot of the UI.
|
||||
You can find this information at `/config/developer-tools/state`
|
||||
You can find this information at `/developer-tools/state`
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
56
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -21,14 +21,6 @@
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If your PR includes visual changes, please add screenshots or a short video
|
||||
showing the before and after. This helps reviewers understand the impact of
|
||||
your changes.
|
||||
Note: Remove this section if this PR has no visual changes.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to the Home Assistant frontend?
|
||||
@@ -43,6 +35,16 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example configuration
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
@@ -52,8 +54,6 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
@@ -61,50 +61,18 @@
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
||||
123
.github/copilot-instructions.md
vendored
123
.github/copilot-instructions.md
vendored
@@ -22,13 +22,11 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
@@ -156,7 +154,7 @@ try {
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||
@@ -194,13 +192,13 @@ The View Transitions API creates smooth animations between DOM state changes. Wh
|
||||
- **Utility wrapper**: `src/common/util/view-transition.ts` - `withViewTransition()` function with graceful fallback
|
||||
- **Real-world example**: `src/util/launch-screen.ts` - Launch screen fade pattern with browser support detection
|
||||
- **Animation keyframes**: `src/resources/theme/animations.globals.ts` - Global `fade-in`, `fade-out`, `scale` animations
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-duration-fast` (150ms), `--ha-animation-duration-normal` (250ms), `--ha-animation-duration-slow` (350ms) (all respect `prefers-reduced-motion`)
|
||||
- **Animation duration**: `src/resources/theme/core.globals.ts` - `--ha-animation-base-duration` (350ms, respects `prefers-reduced-motion`)
|
||||
|
||||
**Implementation Guidelines:**
|
||||
|
||||
1. Always use `withViewTransition()` wrapper for automatic fallback
|
||||
2. Keep transitions simple (subtle crossfades and fades work best)
|
||||
3. Use `--ha-animation-duration-*` CSS variables for consistent timing (`fast`, `normal`, `slow`)
|
||||
3. Use `--ha-animation-base-duration` CSS variable for consistent timing
|
||||
4. Assign unique `view-transition-name` to elements (must be unique at any given time)
|
||||
5. For Lit components: Override `performUpdate()` or use `::part()` for internal elements
|
||||
|
||||
@@ -214,6 +212,13 @@ By default, `:root` receives `view-transition-name: root`, creating a full-page
|
||||
- Only one view transition can run at a time
|
||||
- **Shadow DOM incompatibility**: View transitions operate at document level and do not work within Shadow DOM due to style isolation ([spec discussion](https://github.com/w3c/csswg-drafts/issues/10303)). For web components, set `view-transition-name` on the `:host` element or use document-level transitions
|
||||
|
||||
**Current Usage & Planned Applications:**
|
||||
|
||||
- Launch screen fade out (implemented)
|
||||
- Automation sidebar transitions (planned - #27238)
|
||||
- More info dialog content changes (planned - #27672)
|
||||
- Toolbar navigation, ha-spinner transitions (planned)
|
||||
|
||||
**Specification & Documentation:**
|
||||
|
||||
For browser support, API details, and current specifications, refer to these authoritative sources (note: check publication dates as specs evolve):
|
||||
@@ -239,7 +244,13 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
|
||||
## Component Library
|
||||
|
||||
### Dialog Component
|
||||
### Dialog Components
|
||||
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
|
||||
@@ -253,7 +264,6 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
**Dialog Implementation Requirements:**
|
||||
|
||||
- Use `ha-dialog` component
|
||||
- Implement `HassDialog<T>` interface
|
||||
- Use `@state() private _open = false` to control dialog visibility
|
||||
- Set `_open = true` in `showDialog()`, `_open = false` in `closeDialog()`
|
||||
@@ -269,6 +279,7 @@ fireEvent(this, "show-dialog", {
|
||||
|
||||
- Use `width` attribute with predefined sizes: `"small"` (320px), `"medium"` (560px - default), `"large"` (720px), or `"full"`
|
||||
- Custom sizing is NOT recommended - use the standard width presets
|
||||
- Example: `<ha-wa-dialog width="small">` for alert/confirmation dialogs
|
||||
|
||||
**Button Appearance Guidelines:**
|
||||
|
||||
@@ -278,9 +289,17 @@ fireEvent(this, "show-dialog", {
|
||||
- **Button sizes**: Use `size="small"` (32px height) or default/medium (40px height)
|
||||
- Always place primary action in `slot="primaryAction"` and secondary in `slot="secondaryAction"` within `ha-dialog-footer`
|
||||
|
||||
**Recent Examples:**
|
||||
|
||||
See these files for current patterns:
|
||||
|
||||
- `src/panels/config/repairs/dialog-repairs-issue.ts`
|
||||
- `src/dialogs/restart/dialog-restart.ts`
|
||||
- `src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts`
|
||||
|
||||
**Gallery Documentation:**
|
||||
|
||||
- `gallery/src/pages/components/ha-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-wa-dialog.markdown`
|
||||
- `gallery/src/pages/components/ha-dialogs.markdown`
|
||||
|
||||
### Form Component (ha-form)
|
||||
@@ -288,6 +307,7 @@ fireEvent(this, "show-dialog", {
|
||||
- Schema-driven using `HaFormSchema[]`
|
||||
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
|
||||
- Built-in validation with error display
|
||||
- Use `dialogInitialFocus` in dialogs
|
||||
- Use `computeLabel`, `computeError`, `computeHelper` for translations
|
||||
|
||||
```typescript
|
||||
@@ -372,6 +392,81 @@ export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Dialog
|
||||
|
||||
```typescript
|
||||
@customElement("dialog-my-feature")
|
||||
export class DialogMyFeature
|
||||
extends LitElement
|
||||
implements HassDialog<MyDialogParams>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
private _params?: MyDialogParams;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
public async showDialog(params: MyDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
header-subtitle=${this._params.subtitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>Dialog content</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [haStyleDialog, css``];
|
||||
}
|
||||
```
|
||||
|
||||
### Dialog Design Guidelines
|
||||
|
||||
- Max width: 560px (Alert/confirmation: 320px fixed width)
|
||||
- Close X-icon on top left (all screen sizes)
|
||||
- Submit button grouped with cancel at bottom right
|
||||
- Keep button labels short: "Save", "Delete", "Enable"
|
||||
- Destructive actions use red warning button
|
||||
- Always use a title (best practice)
|
||||
- Strive for minimalism
|
||||
|
||||
#### Creating a Lovelace Card
|
||||
|
||||
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
|
||||
@@ -462,10 +557,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Pull Requests
|
||||
|
||||
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
@@ -528,6 +619,7 @@ When creating a pull request, you **must** use the PR template located at `.gith
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
@@ -619,10 +711,13 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (integration not component)
|
||||
- [ ] Correct terminology (add-on not addon, integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
- [ ] Dialogs implement HassDialog interface
|
||||
- [ ] Dialog styling uses haStyleDialog
|
||||
- [ ] Dialog accessibility includes dialogInitialFocus
|
||||
- [ ] ha-alert used correctly for messages
|
||||
- [ ] ha-form uses proper schema structure
|
||||
- [ ] Components handle all states (loading, error, unavailable)
|
||||
|
||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -44,3 +44,8 @@ GitHub Actions:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/**
|
||||
- .github/*.yml
|
||||
|
||||
Supervisor:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- hassio/src/**
|
||||
|
||||
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
43
.github/workflows/ci.yaml
vendored
43
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -76,9 +76,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -89,15 +89,32 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
supervisor:
|
||||
name: Build supervisor
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
- name: Build Application
|
||||
run: ./node_modules/.bin/gulp build-hassio
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
|
||||
12
.github/workflows/nightly.yaml
vendored
12
.github/workflows/nightly.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
@@ -20,15 +20,15 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
4
.github/workflows/relative-ci.yaml
vendored
4
.github/workflows/relative-ci.yaml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
bundle: [frontend, supervisor]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
2
.github/workflows/release-drafter.yaml
vendored
2
.github/workflows/release-drafter.yaml
vendored
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
|
||||
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
56
.github/workflows/release.yaml
vendored
56
.github/workflows/release.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.14"
|
||||
PYTHON_VERSION: "3.13"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@@ -19,17 +19,14 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -37,7 +34,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -49,18 +46,14 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install build
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
@@ -84,7 +77,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
@@ -98,9 +91,9 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -118,3 +111,32 @@ jobs:
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
release-supervisor:
|
||||
name: Release supervisor frontend
|
||||
if: github.event.release.prerelease == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
- name: Build supervisor
|
||||
run: hassio/script/build_hassio
|
||||
- name: Tar folder
|
||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
29
.github/workflows/restrict-task-creation.yml
vendored
29
.github/workflows/restrict-task-creation.yml
vendored
@@ -5,38 +5,9 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
add-no-stale:
|
||||
name: Add no-stale label
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To add labels to issues
|
||||
if: >-
|
||||
github.event.issue.type.name == 'Task'
|
||||
|| github.event.issue.type.name == 'Epic'
|
||||
|| github.event.issue.type.name == 'Opportunity'
|
||||
steps:
|
||||
- name: Add no-stale label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['no-stale']
|
||||
});
|
||||
|
||||
check-authorization:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
55
.vscode/tasks.json
vendored
55
.vscode/tasks.json
vendored
@@ -73,6 +73,37 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Supervisor panel",
|
||||
"type": "gulp",
|
||||
"task": "develop-hassio",
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": "build",
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Gallery",
|
||||
"type": "gulp",
|
||||
@@ -215,6 +246,20 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run HA Core for Supervisor in devcontainer",
|
||||
"type": "shell",
|
||||
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Setup and fetch nightly translations",
|
||||
"type": "gulp",
|
||||
@@ -223,6 +268,16 @@
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "supervisorHost",
|
||||
"type": "promptString",
|
||||
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
defaultSemverRangePrefix: ""
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
@@ -14,6 +14,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `script/build_frontend`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
|
||||
|
||||
## Frontend development
|
||||
|
||||
|
||||
@@ -18,14 +18,16 @@ module.exports.sourceMapURL = () => {
|
||||
module.exports.ignorePackages = () => [];
|
||||
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
|
||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
[
|
||||
// Icons in landingpage conflict with icons in HA so we don't load.
|
||||
isLandingPageBuild &&
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||
),
|
||||
isLandingPageBuild &&
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
@@ -36,6 +38,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__DEMO__: false,
|
||||
__SUPERVISOR__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
__HASS_URL__: `\`${
|
||||
@@ -288,6 +291,26 @@ module.exports.config = {
|
||||
};
|
||||
},
|
||||
|
||||
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
|
||||
return {
|
||||
name: "supervisor" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
||||
},
|
||||
outputPath: outputPath(paths.hassio_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild: true,
|
||||
defineOverlay: {
|
||||
__SUPERVISOR__: true,
|
||||
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
gallery({ isProdBuild, latestBuild }) {
|
||||
return {
|
||||
name: "gallery" + nameSuffix(latestBuild),
|
||||
|
||||
@@ -24,6 +24,10 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("clean-hassio", async () =>
|
||||
deleteSync([paths.hassio_output_root, paths.build_dir])
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
|
||||
@@ -43,11 +43,29 @@ const compressAppModernBrotli = () =>
|
||||
const compressAppModernZopfli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressHassioModernBrotli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"brotli"
|
||||
);
|
||||
const compressHassioModernZopfli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"zopfli"
|
||||
);
|
||||
|
||||
const compressAppOtherBrotli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppOtherZopfli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressHassioOtherBrotli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
||||
const compressHassioOtherZopfli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
||||
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
@@ -57,3 +75,12 @@ gulp.task(
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
);
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(
|
||||
compressHassioModernBrotli,
|
||||
compressHassioOtherBrotli,
|
||||
compressHassioModernZopfli,
|
||||
compressHassioOtherZopfli
|
||||
)
|
||||
);
|
||||
|
||||
@@ -266,3 +266,28 @@ gulp.task(
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-hassio-dev",
|
||||
genPagesDevTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
"src",
|
||||
paths.hassio_publicPath
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-hassio-prod",
|
||||
genPagesProdTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
paths.hassio_output_es5,
|
||||
"src"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -123,11 +123,22 @@ gulp.task("copy-translations-app", async () => {
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-landing-page", async () => {
|
||||
const staticDir = paths.landingPage_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
copyFonts(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-app", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
// Basic static files
|
||||
|
||||
45
build-scripts/gulp/hassio.js
Normal file
45
build-scripts/gulp/hassio.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"gen-pages-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
"rspack-watch-hassio"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
"rspack-prod-hassio",
|
||||
"gen-pages-hassio-prod",
|
||||
...// Don't compress running tests
|
||||
(env.isTestBuild() ? [] : ["compress-hassio"])
|
||||
)
|
||||
);
|
||||
@@ -9,6 +9,7 @@ import "./fetch-nightly-translations.js";
|
||||
import "./gallery.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./hassio.js";
|
||||
import "./landing-page.js";
|
||||
import "./locale-data.js";
|
||||
import "./rspack.js";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createCastConfig,
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createHassioConfig,
|
||||
createLandingPageConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
@@ -158,6 +159,31 @@ gulp.task("rspack-prod-cast", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-watch-hassio", () => {
|
||||
// This command will run forever because we don't close compiler
|
||||
rspack(
|
||||
createHassioConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
})
|
||||
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-prod-hassio", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createHassioConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-gallery", () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
|
||||
@@ -170,7 +170,9 @@ const setFragment = (fragment) => async () => {
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" && fragment !== "landing-page";
|
||||
fragment !== "base" &&
|
||||
fragment !== "supervisor" &&
|
||||
fragment !== "landing-page";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
@@ -205,15 +207,18 @@ const createTranslations = async () => {
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and landing-page to create the base translations
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
"landing-page": undefined,
|
||||
supervisor: undefined,
|
||||
}),
|
||||
"",
|
||||
];
|
||||
case "supervisor":
|
||||
// Supervisor key is at the top level
|
||||
return [flatten(data.supervisor), ""];
|
||||
case "landing-page":
|
||||
// landing-page key is at the top level
|
||||
return [flatten(data["landing-page"]), ""];
|
||||
@@ -313,6 +318,11 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(setFragment("supervisor"), "build-translations")
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page-translations",
|
||||
gulp.series(setFragment("landing-page"), "build-translations")
|
||||
|
||||
@@ -49,5 +49,15 @@ module.exports = {
|
||||
"../landing-page/dist/static"
|
||||
),
|
||||
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
|
||||
hassio_output_latest: path.resolve(
|
||||
__dirname,
|
||||
"../hassio/build/frontend_latest"
|
||||
),
|
||||
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
||||
hassio_publicPath: "/api/hassio/app",
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ const createRspackConfig = ({
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild,
|
||||
isLandingPageBuild,
|
||||
dontHash,
|
||||
}) => {
|
||||
@@ -167,12 +168,12 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
bundle.emptyPackages({ isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
),
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
new StatsWriterPlugin({
|
||||
@@ -200,7 +201,6 @@ const createRspackConfig = ({
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
"lit/directives/until$": "lit/directives/until.js",
|
||||
"lit/directives/ref$": "lit/directives/ref.js",
|
||||
"lit/directives/class-map$": "lit/directives/class-map.js",
|
||||
"lit/directives/style-map$": "lit/directives/style-map.js",
|
||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||
@@ -209,9 +209,7 @@ const createRspackConfig = ({
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
@@ -219,42 +217,6 @@ const createRspackConfig = ({
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
"@formatjs/intl-durationformat/should-polyfill$":
|
||||
"@formatjs/intl-durationformat/should-polyfill.js",
|
||||
"@formatjs/intl-durationformat/polyfill-force$":
|
||||
"@formatjs/intl-durationformat/polyfill-force.js",
|
||||
"@formatjs/intl-datetimeformat/should-polyfill":
|
||||
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-datetimeformat/polyfill-force":
|
||||
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
||||
"@formatjs/intl-displaynames/should-polyfill":
|
||||
"@formatjs/intl-displaynames/should-polyfill.js",
|
||||
"@formatjs/intl-displaynames/polyfill-force":
|
||||
"@formatjs/intl-displaynames/polyfill-force.js",
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
||||
"@formatjs/intl-listformat/should-polyfill":
|
||||
"@formatjs/intl-listformat/should-polyfill.js",
|
||||
"@formatjs/intl-listformat/polyfill-force":
|
||||
"@formatjs/intl-listformat/polyfill-force.js",
|
||||
"@formatjs/intl-locale/should-polyfill":
|
||||
"@formatjs/intl-locale/should-polyfill.js",
|
||||
"@formatjs/intl-locale/polyfill-force":
|
||||
"@formatjs/intl-locale/polyfill-force.js",
|
||||
"@formatjs/intl-numberformat/should-polyfill":
|
||||
"@formatjs/intl-numberformat/should-polyfill.js",
|
||||
"@formatjs/intl-numberformat/polyfill-force":
|
||||
"@formatjs/intl-numberformat/polyfill-force.js",
|
||||
"@formatjs/intl-pluralrules/should-polyfill":
|
||||
"@formatjs/intl-pluralrules/should-polyfill.js",
|
||||
"@formatjs/intl-pluralrules/polyfill-force":
|
||||
"@formatjs/intl-pluralrules/polyfill-force.js",
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill":
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
@@ -321,6 +283,21 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
||||
|
||||
const createHassioConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}) =>
|
||||
createRspackConfig(
|
||||
bundle.config.hassio({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
})
|
||||
);
|
||||
|
||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
@@ -331,6 +308,7 @@ module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
|
||||
@@ -206,7 +206,7 @@ class HcCast extends LitElement {
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
|
||||
@@ -5,19 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { framework } from "./cast_framework";
|
||||
import { CAST_NS } from "../../../src/cast/const";
|
||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||
import "../../../src/resources/custom-card-support";
|
||||
import { castContext } from "./cast_context";
|
||||
import { framework } from "./cast_framework";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import type { ReceivedMessage } from "./types";
|
||||
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
@@ -39,8 +40,7 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -89,30 +89,31 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
// type definition is wrong, should be "JSON" instead of "json"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
}
|
||||
const msg = ev.data as HassMessage;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
});
|
||||
);
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -126,26 +127,24 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
// @ts-ignore
|
||||
media.hlsVideoSegmentFormat =
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState ===
|
||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
||||
6
cast/src/receiver/types.ts
Normal file
6
cast/src/receiver/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ReceivedMessage<T> {
|
||||
gj: boolean;
|
||||
data: T;
|
||||
senderId: string;
|
||||
type: "message";
|
||||
}
|
||||
@@ -9,14 +9,11 @@ import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "./stubs/device_registry";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
import { mockLovelace } from "./stubs/lovelace";
|
||||
@@ -63,9 +60,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -14,28 +14,40 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
flow_from: [
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
flow_to: [
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_compensation:
|
||||
"sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
power: [
|
||||
{ stat_rate: "sensor.power_grid" },
|
||||
{ stat_rate: "sensor.power_grid_return" },
|
||||
],
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let sidebarChangeCallback;
|
||||
let changeFunction;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
sidebarChangeCallback?.({
|
||||
changeFunction?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -14,34 +16,15 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS(
|
||||
"frontend/subscribe_system_data",
|
||||
(_msg, currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: currentHass.systemData,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
}
|
||||
);
|
||||
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -7,18 +7,8 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, _hass, onChange) => {
|
||||
let result = msg.template;
|
||||
// Simple variable substitution for demo purposes
|
||||
if (msg.variables) {
|
||||
for (const [key, value] of Object.entries(msg.variables)) {
|
||||
result = result.replace(
|
||||
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
onChange!({
|
||||
result,
|
||||
result: msg.template,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@@ -12,7 +12,6 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -44,6 +43,7 @@ export default tseslint.config(
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
@@ -187,19 +187,5 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
},
|
||||
rules: {
|
||||
"html/no-invalid-attr-value": "error",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], this._action)
|
||||
? describeAction(this.hass, [], [], {}, this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -10,9 +10,7 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@@ -215,7 +215,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<li>
|
||||
<strong>Dialog mode:</strong> Used on larger screens (width >
|
||||
870px and height > 500px). Renders as a centered dialog using
|
||||
<code>ha-dialog</code>.
|
||||
<code>ha-wa-dialog</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bottom sheet mode:</strong> Used on mobile devices and
|
||||
@@ -394,7 +394,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
|
||||
<p>
|
||||
If you don't need responsive behavior, use
|
||||
<code>ha-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-bottom-sheet</code> for mobile-only sheets.
|
||||
</p>
|
||||
|
||||
@@ -410,6 +410,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
width="medium"
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
>
|
||||
@@ -446,7 +447,7 @@ export class DemoHaAdaptiveDialog extends LitElement {
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
This component combines <code>ha-dialog</code> and
|
||||
This component combines <code>ha-wa-dialog</code> and
|
||||
<code>ha-bottom-sheet</code> with automatic mode switching based on
|
||||
screen size.
|
||||
</p>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Dialog (ha-dialog)
|
||||
---
|
||||
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -169,7 +169,7 @@ const SCHEMAS: {
|
||||
{
|
||||
title: "Selectors",
|
||||
translations: {
|
||||
app: "App",
|
||||
addon: "Addon",
|
||||
entity: "Entity",
|
||||
device: "Device",
|
||||
area: "Area",
|
||||
@@ -188,7 +188,7 @@ const SCHEMAS: {
|
||||
entities: "Entities",
|
||||
},
|
||||
schema: [
|
||||
{ name: "app", selector: { app: {} } },
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "Attribute",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
@@ -40,9 +40,6 @@ const ENTITIES = [
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
getEntity("number", "number", 5, {
|
||||
friendly_name: "Number",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
@@ -239,7 +236,7 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
app: { name: "App", selector: { app: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
@@ -380,33 +377,6 @@ const SCHEMAS: {
|
||||
name: "Constant",
|
||||
selector: { constant: { value: true, label: "Yes!" } },
|
||||
},
|
||||
choose: {
|
||||
name: "Choose",
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
number: {
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entity: {
|
||||
selector: {
|
||||
entity: {
|
||||
filter: {
|
||||
domain: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Dialog (ha-wa-dialog)
|
||||
---
|
||||
@@ -6,7 +6,7 @@ import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dialog-footer";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-wa-dialog";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
@@ -22,14 +22,14 @@ type DialogType =
|
||||
| "form"
|
||||
| "actions";
|
||||
|
||||
@customElement("demo-components-ha-dialog")
|
||||
export class DemoHaDialog extends LitElement {
|
||||
@customElement("demo-components-ha-wa-dialog")
|
||||
export class DemoHaWaDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Dialog <code><ha-dialog></code></h1>
|
||||
<h1>Dialog <code><ha-wa-dialog></code></h1>
|
||||
|
||||
<p class="subtitle">Dialog component built with WebAwesome.</p>
|
||||
|
||||
@@ -53,24 +53,24 @@ export class DemoHaDialog extends LitElement {
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Basic dialog with subtitle"
|
||||
header-subtitle="This is a basic dialog with a subtitle below"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Dialog with subtitle above"
|
||||
header-subtitle="This is a basic dialog with a subtitle above"
|
||||
@@ -78,9 +78,9 @@ export class DemoHaDialog extends LitElement {
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Dialog with form"
|
||||
header-subtitle="This is a dialog with a form and a footer"
|
||||
@@ -91,18 +91,17 @@ export class DemoHaDialog extends LitElement {
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Dialog with actions"
|
||||
header-subtitle="This is a dialog with header actions"
|
||||
@@ -114,7 +113,7 @@ export class DemoHaDialog extends LitElement {
|
||||
</div>
|
||||
|
||||
<div>Dialog content</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<h2>Design</h2>
|
||||
|
||||
@@ -229,19 +228,19 @@ export class DemoHaDialog extends LitElement {
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
<th>Appearance to use</th>
|
||||
<th>Variant to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>secondaryAction</code></td>
|
||||
<td>The secondary action button(s).</td>
|
||||
<td><code>appearance="plain"</code></td>
|
||||
<td><code>plain</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>primaryAction</code></td>
|
||||
<td>The primary action button(s).</td>
|
||||
<td>Default (no appearance attribute)</td>
|
||||
<td><code>accent</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -250,7 +249,7 @@ export class DemoHaDialog extends LitElement {
|
||||
|
||||
<h3>Example Usage</h3>
|
||||
|
||||
<pre><code><ha-dialog
|
||||
<pre><code><ha-wa-dialog
|
||||
open
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
@@ -262,18 +261,12 @@ export class DemoHaDialog extends LitElement {
|
||||
</div>
|
||||
<div>Dialog content</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
<ha-button data-dialog="close" slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
Cancel
|
||||
</ha-button>
|
||||
<ha-button data-dialog="close" slot="primaryAction">
|
||||
Submit
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog></code></pre>
|
||||
</ha-wa-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
@@ -521,6 +514,6 @@ export class DemoHaDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-dialog": DemoHaDialog;
|
||||
"demo-components-ha-wa-dialog": DemoHaWaDialog;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
9
hassio/config.cjs
Normal file
9
hassio/config.cjs
Normal file
@@ -0,0 +1,9 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
// Target directory for the build.
|
||||
buildDir: path.resolve(__dirname, "build"),
|
||||
nodeDir: path.resolve(__dirname, "../node_modules"),
|
||||
// Path where the Hass.io frontend will be publicly available.
|
||||
publicPath: "/api/hassio/app",
|
||||
};
|
||||
9
hassio/script/build_hassio
Executable file
9
hassio/script/build_hassio
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Builds the Hass.io app for production
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-hassio
|
||||
9
hassio/script/develop
Executable file
9
hassio/script/develop
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Run the Hass.io development server
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-hassio
|
||||
@@ -3,20 +3,24 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-card";
|
||||
import type { HassioAddonRepository } from "../../../data/hassio/addon";
|
||||
import type { StoreAddon } from "../../../data/supervisor/store";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/supervisor-apps-card-content";
|
||||
import { filterAndSort } from "./components/supervisor-apps-filter";
|
||||
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { filterAndSort } from "../components/hassio-filter-addons";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("supervisor-apps-repository")
|
||||
export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
@customElement("hassio-addon-repository")
|
||||
export class HassioAddonRepositoryEl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public repo!: HassioAddonRepository;
|
||||
|
||||
@property({ attribute: false }) public addons!: StoreAddon[];
|
||||
@@ -46,12 +50,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
return html`
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.store.no_results_found",
|
||||
{
|
||||
repository: repo.name,
|
||||
}
|
||||
)}
|
||||
${this.supervisor.localize("store.no_results_found", {
|
||||
repository: repo.name,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -69,7 +70,7 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<supervisor-apps-card-content
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
@@ -79,19 +80,13 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.update_available
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.state.update_available"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.state.installed"
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: addon.available
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.state.not_installed"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.state.not_available"
|
||||
)}
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
@@ -99,7 +94,11 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${addon.icon
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
@@ -110,7 +109,7 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></supervisor-apps-card-content>
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
@@ -121,12 +120,12 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
}
|
||||
|
||||
private _addonTapped(ev) {
|
||||
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
@@ -145,6 +144,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-apps-repository": SupervisorAppsRepositoryEl;
|
||||
"hassio-addon-repository": HassioAddonRepositoryEl;
|
||||
}
|
||||
}
|
||||
248
hassio/src/addon-store/hassio-addon-store.ts
Normal file
248
hassio/src/addon-store/hassio-addon-store.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/search-input";
|
||||
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import "./hassio-addon-repository";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "local") {
|
||||
return 1;
|
||||
}
|
||||
if (a.slug === "core") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "core") {
|
||||
return 1;
|
||||
}
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
};
|
||||
|
||||
@customElement("hassio-addon-store")
|
||||
export class HassioAddonStore extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public async refreshData() {
|
||||
try {
|
||||
await reloadHassioAddons(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let repos: (TemplateResult | typeof nothing)[] = [];
|
||||
|
||||
if (this.supervisor.store.repositories) {
|
||||
repos = this.addonRepositories(
|
||||
this.supervisor.store.repositories,
|
||||
this.supervisor.store.addons,
|
||||
this._filter
|
||||
);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.header=${this.supervisor.localize("panel.store")}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("store.check_updates")}
|
||||
</ha-list-item>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("store.repositories")}
|
||||
</ha-list-item>
|
||||
${this.hass.userData?.showAdvanced &&
|
||||
atLeastVersion(this.hass.config.version, 0, 117)
|
||||
? html`<ha-list-item>
|
||||
${this.supervisor.localize("store.registries")}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
${repos.length === 0
|
||||
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
</div>
|
||||
|
||||
${repos}
|
||||
`}
|
||||
${!this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="advanced">
|
||||
<a href="/profile" target="_top">
|
||||
${this.supervisor.localize("store.missing_addons")}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const repositoryUrl = extractSearchParam("repository_url");
|
||||
navigate("/hassio/store", { replace: true });
|
||||
if (repositoryUrl) {
|
||||
this._manageRepositories(repositoryUrl);
|
||||
}
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private addonRepositories = memoizeOne(
|
||||
(
|
||||
repositories: HassioAddonRepository[],
|
||||
addons: StoreAddon[],
|
||||
filter?: string
|
||||
) =>
|
||||
repositories.sort(sortRepos).map((repo) => {
|
||||
const filteredAddons = addons.filter(
|
||||
(addon) => addon.repository === repo.slug
|
||||
);
|
||||
|
||||
return filteredAddons.length !== 0
|
||||
? html`
|
||||
<hassio-addon-repository
|
||||
.hass=${this.hass}
|
||||
.repo=${repo}
|
||||
.addons=${filteredAddons}
|
||||
.filter=${filter!}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addon-repository>
|
||||
`
|
||||
: nothing;
|
||||
})
|
||||
);
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this.refreshData();
|
||||
break;
|
||||
case 1:
|
||||
this._manageRepositoriesClicked();
|
||||
break;
|
||||
case 2:
|
||||
this._manageRegistries();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
private _manageRepositoriesClicked() {
|
||||
this._manageRepositories();
|
||||
}
|
||||
|
||||
private _manageRepositories(url?: string) {
|
||||
showRepositoriesDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
private _manageRegistries() {
|
||||
showRegistriesDialog(this, { supervisor: this.supervisor });
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "supervisor",
|
||||
});
|
||||
}
|
||||
|
||||
private _filterChanged(e) {
|
||||
this._filter = e.detail.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
.advanced {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.advanced a {
|
||||
margin-left: 0.5em;
|
||||
margin-inline-start: 0.5em;
|
||||
margin-inline-end: initial;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-store": HassioAddonStore;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,31 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-select";
|
||||
import type {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
|
||||
import type { HassioHardwareAudioDevice } from "../../../../../data/hassio/hardware";
|
||||
import { fetchHassioHardwareAudio } from "../../../../../data/hassio/hardware";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
|
||||
import type { HassioHardwareAudioDevice } from "../../../../src/data/hassio/hardware";
|
||||
import { fetchHassioHardwareAudio } from "../../../../src/data/hassio/hardware";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("supervisor-app-audio")
|
||||
class SupervisorAppAudio extends LitElement {
|
||||
@customElement("hassio-addon-audio")
|
||||
class HassioAddonAudio extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -40,9 +44,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.header"
|
||||
)}
|
||||
.header=${this.supervisor.localize("addon.configuration.audio.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
@@ -50,31 +52,43 @@ class SupervisorAppAudio extends LitElement {
|
||||
: nothing}
|
||||
${this._inputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.input"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.input"
|
||||
)}
|
||||
@selected=${this._setInputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedInput!}
|
||||
.disabled=${this.disabled}
|
||||
.options=${this._inputDevices.map((item) => ({
|
||||
value: item.device || "",
|
||||
label: item.name,
|
||||
}))}
|
||||
>
|
||||
${this._inputDevices.map(
|
||||
(item) => html`
|
||||
<ha-list-item .value=${item.device || ""}>
|
||||
${item.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
${this._outputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.output"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.output"
|
||||
)}
|
||||
@selected=${this._setOutputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedOutput!}
|
||||
.disabled=${this.disabled}
|
||||
.options=${this._outputDevices.map((item) => ({
|
||||
value: item.device || "",
|
||||
label: item.name,
|
||||
}))}
|
||||
>
|
||||
${this._outputDevices.map(
|
||||
(item) => html`
|
||||
<ha-list-item .value=${item.device || ""}
|
||||
>${item.name}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -82,7 +96,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._saveSettings}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -92,7 +106,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host,
|
||||
ha-card {
|
||||
@@ -103,10 +117,9 @@ class SupervisorAppAudio extends LitElement {
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
ha-select:last-child {
|
||||
margin-top: var(--ha-space-2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -119,14 +132,14 @@ class SupervisorAppAudio extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _setInputDevice(ev: HaSelectSelectEvent): void {
|
||||
const device = ev.detail.value;
|
||||
this._selectedInput = device ?? null;
|
||||
private _setInputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
this._selectedInput = device;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev: HaSelectSelectEvent): void {
|
||||
const device = ev.detail.value;
|
||||
this._selectedOutput = device ?? null;
|
||||
private _setOutputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
|
||||
private async _addonChanged(): Promise<void> {
|
||||
@@ -140,9 +153,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.default"
|
||||
),
|
||||
name: this.supervisor.localize("addon.configuration.audio.default"),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -159,9 +170,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
this._inputDevices = [noDevice, ...input];
|
||||
this._outputDevices = [noDevice, ...output];
|
||||
} catch {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.failed_to_load_hardware"
|
||||
);
|
||||
this._error = "Failed to fetch audio hardware";
|
||||
this._inputDevices = [noDevice];
|
||||
this._outputDevices = [noDevice];
|
||||
}
|
||||
@@ -185,12 +194,10 @@ class SupervisorAppAudio extends LitElement {
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.audio.failed_to_save"
|
||||
);
|
||||
this._error = "Failed to set addon audio device";
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
@@ -199,6 +206,6 @@ class SupervisorAppAudio extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-audio": SupervisorAppAudio;
|
||||
"hassio-addon-audio": HassioAddonAudio;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import "../info/supervisor-app-system-managed";
|
||||
import "./supervisor-app-audio";
|
||||
import "./supervisor-app-config";
|
||||
import "./supervisor-app-network";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../info/hassio-addon-system-managed";
|
||||
import "./hassio-addon-audio";
|
||||
import "./hassio-addon-config";
|
||||
import "./hassio-addon-network";
|
||||
|
||||
@customElement("supervisor-app-config-tab")
|
||||
class SupervisorAppConfigDashboard extends LitElement {
|
||||
@customElement("hassio-addon-config-tab")
|
||||
class HassioAddonConfigDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@@ -35,49 +38,50 @@ class SupervisorAppConfigDashboard extends LitElement {
|
||||
${this.addon.system_managed &&
|
||||
(hasConfiguration || this.addon.network || this.addon.audio)
|
||||
? html`
|
||||
<supervisor-app-system-managed
|
||||
.hass=${this.hass}
|
||||
<hassio-addon-system-managed
|
||||
.supervisor=${this.supervisor}
|
||||
.narrow=${this.narrow}
|
||||
.hideButton=${this.controlEnabled}
|
||||
></supervisor-app-system-managed>
|
||||
></hassio-addon-system-managed>
|
||||
`
|
||||
: nothing}
|
||||
${hasConfiguration || this.addon.network || this.addon.audio
|
||||
? html`
|
||||
${hasConfiguration
|
||||
? html`
|
||||
<supervisor-app-config
|
||||
<hassio-addon-config
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></supervisor-app-config>
|
||||
></hassio-addon-config>
|
||||
`
|
||||
: nothing}
|
||||
${this.addon.network
|
||||
? html`
|
||||
<supervisor-app-network
|
||||
<hassio-addon-network
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></supervisor-app-network>
|
||||
></hassio-addon-network>
|
||||
`
|
||||
: nothing}
|
||||
${this.addon.audio
|
||||
? html`
|
||||
<supervisor-app-audio
|
||||
<hassio-addon-audio
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></supervisor-app-audio>
|
||||
></hassio-addon-audio>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.no_configuration"
|
||||
)}
|
||||
: this.supervisor.localize("addon.configuration.no_configuration")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -85,17 +89,17 @@ class SupervisorAppConfigDashboard extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
supervisor-app-network,
|
||||
supervisor-app-audio,
|
||||
supervisor-app-config {
|
||||
margin-bottom: var(--ha-space-6);
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -104,6 +108,6 @@ class SupervisorAppConfigDashboard extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-config-tab": SupervisorAppConfigDashboard;
|
||||
"hassio-addon-config-tab": HassioAddonConfigDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,42 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { DEFAULT_SCHEMA, Type } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormSchema,
|
||||
} from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
|
||||
HaFormDataContainer,
|
||||
} from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
|
||||
import type {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
setHassioAddonOption,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import type { ObjectSelector, Selector } from "../../../../../data/selector";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
|
||||
|
||||
const SUPPORTED_UI_TYPES = [
|
||||
"string",
|
||||
@@ -55,12 +56,14 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@customElement("supervisor-app-config")
|
||||
class SupervisorAppConfig extends LitElement {
|
||||
@customElement("hassio-addon-config")
|
||||
class HassioAddonConfig extends LitElement {
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
@@ -216,39 +219,31 @@ class SupervisorAppConfig extends LitElement {
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.header"
|
||||
)}
|
||||
${this.supervisor.localize("addon.configuration.options.header")}
|
||||
</h2>
|
||||
<div class="card-menu">
|
||||
<ha-dropdown @wa-select=${this._handleAction}>
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml"
|
||||
.disabled=${!this._canShowSchema || this.disabled}
|
||||
>
|
||||
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
|
||||
${this._yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.edit_in_ui"
|
||||
? this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_ui"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.edit_in_yaml"
|
||||
: this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_yaml"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
value="reset"
|
||||
.variant=${!this.disabled ? "danger" : "default"}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class=${!this.disabled ? "warning" : ""}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.reset_defaults"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${this.supervisor.localize("common.reset_defaults")}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -283,8 +278,8 @@ class SupervisorAppConfig extends LitElement {
|
||||
? ""
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.invalid_yaml"
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.options.invalid_yaml"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
@@ -292,8 +287,8 @@ class SupervisorAppConfig extends LitElement {
|
||||
${hasHiddenOptions
|
||||
? html`<ha-formfield
|
||||
class="show-additional"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.options.show_unused_optional"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.options.show_unused_optional"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -310,7 +305,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
!this._configHasChanged ||
|
||||
!this._valid}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -319,13 +314,11 @@ class SupervisorAppConfig extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._canShowSchema =
|
||||
this.addon.schema !== null &&
|
||||
!this.addon.schema!.find(
|
||||
(entry) =>
|
||||
// @ts-ignore
|
||||
!SUPPORTED_UI_TYPES.includes(entry.type)
|
||||
);
|
||||
this._canShowSchema = !this.addon.schema!.find(
|
||||
(entry) =>
|
||||
// @ts-ignore
|
||||
!SUPPORTED_UI_TYPES.includes(entry.type)
|
||||
);
|
||||
this._yamlMode = !this._canShowSchema;
|
||||
}
|
||||
|
||||
@@ -347,19 +340,14 @@ class SupervisorAppConfig extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "toggle_yaml":
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
return;
|
||||
case "reset":
|
||||
break;
|
||||
case 1:
|
||||
this._resetTapped(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,14 +371,10 @@ class SupervisorAppConfig extends LitElement {
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.confirm.reset_options.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.confirm.reset_options.text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.reset_options"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
title: this.supervisor.localize("confirm.reset_options.title"),
|
||||
text: this.supervisor.localize("confirm.reset_options.text"),
|
||||
confirmText: this.supervisor.localize("common.reset_options"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
@@ -413,12 +397,9 @@ class SupervisorAppConfig extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_reset",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
@@ -456,15 +437,12 @@ class SupervisorAppConfig extends LitElement {
|
||||
|
||||
this._configHasChanged = false;
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
eventdata.success = false;
|
||||
}
|
||||
button.progress = false;
|
||||
@@ -474,7 +452,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -492,7 +470,7 @@ class SupervisorAppConfig extends LitElement {
|
||||
z-index: 3;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-dropdown-item[disabled] {
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
.header {
|
||||
@@ -524,6 +502,6 @@ class SupervisorAppConfig extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-config": SupervisorAppConfig;
|
||||
"hassio-addon-config": HassioAddonConfig;
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,31 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../../components/ha-form/types";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type {
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
|
||||
@customElement("supervisor-app-network")
|
||||
class SupervisorAppNetwork extends LitElement {
|
||||
@customElement("hassio-addon-network")
|
||||
class HassioAddonNetwork extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -36,6 +39,11 @@ class SupervisorAppNetwork extends LitElement {
|
||||
|
||||
@state() private _config?: Record<string, any>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setNetworkConfig();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
@@ -48,14 +56,14 @@ class SupervisorAppNetwork extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.network.header"
|
||||
.header=${this.supervisor.localize(
|
||||
"addon.configuration.network.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.network.introduction"
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.network.introduction"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
@@ -78,8 +86,8 @@ class SupervisorAppNetwork extends LitElement {
|
||||
${hasHiddenOptions
|
||||
? html`<ha-formfield
|
||||
class="show-optional"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.network.show_disabled"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.network.show_disabled"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -96,23 +104,21 @@ class SupervisorAppNetwork extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._resetTapped}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.configuration.network.reset_defaults"
|
||||
)}
|
||||
${this.supervisor.localize("common.reset_defaults")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || this.disabled}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
protected update(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._setNetworkConfig();
|
||||
}
|
||||
@@ -178,15 +184,12 @@ class SupervisorAppNetwork extends LitElement {
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_reset",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
@@ -223,15 +226,12 @@ class SupervisorAppNetwork extends LitElement {
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
@@ -239,7 +239,7 @@ class SupervisorAppNetwork extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -261,6 +261,6 @@ class SupervisorAppNetwork extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-network": SupervisorAppNetwork;
|
||||
"hassio-addon-network": HassioAddonNetwork;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../src/components/ha-card";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-markdown";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { fetchHassioAddonDocumentation } from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import "../../../../../layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import { fetchHassioAddonDocumentation } from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
@customElement("supervisor-app-documentation-tab")
|
||||
class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
@customElement("hassio-addon-documentation-tab")
|
||||
class HassioAddonDocumentationDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -54,19 +57,18 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -80,8 +82,8 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
this.addon!.slug
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.documentation.get_documentation",
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_documentation",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
@@ -90,6 +92,6 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-documentation-tab": SupervisorAppDocumentationDashboard;
|
||||
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
|
||||
}
|
||||
}
|
||||
294
hassio/src/addon-view/hassio-addon-dashboard.ts
Normal file
294
hassio/src/addon-view/hassio-addon-dashboard.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import {
|
||||
mdiCogs,
|
||||
mdiFileDocument,
|
||||
mdiInformationVariant,
|
||||
mdiMathLog,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchAddonInfo,
|
||||
fetchHassioAddonInfo,
|
||||
fetchHassioAddonsInfo,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchSupervisorStore,
|
||||
} from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-error-screen";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./config/hassio-addon-audio";
|
||||
import "./config/hassio-addon-config";
|
||||
import "./config/hassio-addon-network";
|
||||
import "./hassio-addon-router";
|
||||
import "./info/hassio-addon-info";
|
||||
|
||||
@customElement("hassio-addon-dashboard")
|
||||
class HassioAddonDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public addon?:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state()
|
||||
private _controlEnabled = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _backPath = new URLSearchParams(window.parent.location.search).get(
|
||||
"store"
|
||||
)
|
||||
? "/hassio/store"
|
||||
: "/hassio/dashboard";
|
||||
|
||||
private _computeTail = memoizeOne((route: Route) => {
|
||||
const dividerPos = route.path.indexOf("/", 1);
|
||||
return dividerPos === -1
|
||||
? {
|
||||
prefix: route.prefix + route.path,
|
||||
path: "",
|
||||
}
|
||||
: {
|
||||
prefix: route.prefix + route.path.substr(0, dividerPos),
|
||||
path: route.path.substr(dividerPos),
|
||||
};
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<hass-error-screen
|
||||
.error=${this._error}
|
||||
></hass-error-screen>`;
|
||||
}
|
||||
|
||||
if (!this.addon || !this.supervisor?.addon) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const addonTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "addon.panel.info",
|
||||
path: `/hassio/addon/${this.addon.slug}/info`,
|
||||
iconPath: mdiInformationVariant,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.addon.documentation) {
|
||||
addonTabs.push({
|
||||
translationKey: "addon.panel.documentation",
|
||||
path: `/hassio/addon/${this.addon.slug}/documentation`,
|
||||
iconPath: mdiFileDocument,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addon.version) {
|
||||
addonTabs.push(
|
||||
{
|
||||
translationKey: "addon.panel.configuration",
|
||||
path: `/hassio/addon/${this.addon.slug}/config`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
{
|
||||
translationKey: "addon.panel.log",
|
||||
path: `/hassio/addon/${this.addon.slug}/logs`,
|
||||
iconPath: mdiMathLog,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const route = this._computeTail(this.route);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${route}
|
||||
.tabs=${addonTabs}
|
||||
.backPath=${this._backPath}
|
||||
supervisor
|
||||
>
|
||||
<span slot="header">${this.addon.name}</span>
|
||||
<hassio-addon-router
|
||||
.route=${route}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.addon=${this.addon}
|
||||
.controlEnabled=${this._controlEnabled}
|
||||
@system-managed-take-control=${this._enableControl}
|
||||
></hassio-addon-router>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _enableControl() {
|
||||
this._controlEnabled = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
@media only screen and (max-width: 600px) {
|
||||
hassio-addon-info,
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
if (this.route.path === "") {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
const requestedAddonRepository = extractSearchParam("repository_url");
|
||||
if (requestedAddonRepository) {
|
||||
const storeInfo = await fetchSupervisorStore(this.hass);
|
||||
if (
|
||||
!storeInfo.repositories.find(
|
||||
(repo) => repo.source === requestedAddonRepository
|
||||
)
|
||||
) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_addon_repository_description",
|
||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.add"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"my.error_repository_not_found"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await addStoreRepository(this.hass, requestedAddonRepository);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedAddon) {
|
||||
const store = await fetchSupervisorStore(this.hass);
|
||||
const validAddon = store.addons.some(
|
||||
(addon) => addon.slug === requestedAddon
|
||||
);
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
} else {
|
||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private async _apiCalled(ev): Promise<void> {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSplit: string[] = ev.detail.path?.split("/");
|
||||
|
||||
if (!pathSplit || pathSplit.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path: string = pathSplit[pathSplit.length - 1];
|
||||
|
||||
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
}
|
||||
|
||||
if (path === "uninstall") {
|
||||
if (this.isConnected) {
|
||||
navigate(this._backPath);
|
||||
}
|
||||
} else if (path === "install") {
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
|
||||
} else {
|
||||
await this._routeDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("route") && !this.addon) {
|
||||
this._routeDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async _routeDataChanged(): Promise<void> {
|
||||
const addon = this.route.path.split("/")[1];
|
||||
if (!addon) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!this.supervisor.addon) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { addon: addonsInfo });
|
||||
}
|
||||
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
|
||||
} catch (err: any) {
|
||||
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
|
||||
this.addon = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-dashboard": HassioAddonDashboard;
|
||||
}
|
||||
}
|
||||
62
hassio/src/addon-view/hassio-addon-router.ts
Normal file
62
hassio/src/addon-view/hassio-addon-router.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import type { RouterOptions } from "../../../src/layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../src/layouts/hass-router-page";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./config/hassio-addon-config-tab";
|
||||
import "./documentation/hassio-addon-documentation-tab";
|
||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||
import "./info/hassio-addon-info-tab";
|
||||
import "./log/hassio-addon-log-tab";
|
||||
|
||||
@customElement("hassio-addon-router")
|
||||
class HassioAddonRouter extends HassRouterPage {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ type: Boolean, attribute: "control-enabled" })
|
||||
public controlEnabled = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "info",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
info: {
|
||||
tag: "hassio-addon-info-tab",
|
||||
},
|
||||
documentation: {
|
||||
tag: "hassio-addon-documentation-tab",
|
||||
},
|
||||
config: {
|
||||
tag: "hassio-addon-config-tab",
|
||||
},
|
||||
logs: {
|
||||
tag: "hassio-addon-log-tab",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
el.addon = this.addon;
|
||||
el.narrow = this.narrow;
|
||||
el.controlEnabled = this.controlEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-router": HassioAddonRouter;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import "./supervisor-app-info";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "./hassio-addon-info";
|
||||
|
||||
@customElement("supervisor-app-info-tab")
|
||||
class SupervisorAppInfoDashboard extends LitElement {
|
||||
@customElement("hassio-addon-info-tab")
|
||||
class HassioAddonInfoDashboard extends LitElement {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@property({ type: Boolean, attribute: "control-enabled" })
|
||||
@@ -28,13 +31,14 @@ class SupervisorAppInfoDashboard extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<supervisor-app-info
|
||||
<hassio-addon-info
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.addon=${this.addon}
|
||||
.controlEnabled=${this.controlEnabled}
|
||||
></supervisor-app-info>
|
||||
></hassio-addon-info>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -42,11 +46,11 @@ class SupervisorAppInfoDashboard extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
`,
|
||||
@@ -56,6 +60,6 @@ class SupervisorAppInfoDashboard extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-info-tab": SupervisorAppInfoDashboard;
|
||||
"hassio-addon-info-tab": HassioAddonInfoDashboard;
|
||||
}
|
||||
}
|
||||
@@ -27,28 +27,28 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../../../common/config/version";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../../components/chips/ha-chip-set";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-markdown";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../../components/ha-switch";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../../src/common/navigate";
|
||||
import { capitalizeFirstLetter } from "../../../../src/common/string/capitalize-first-letter";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/chips/ha-assist-chip";
|
||||
import "../../../../src/components/chips/ha-chip-set";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../src/components/ha-switch";
|
||||
import type {
|
||||
AddonCapability,
|
||||
HassioAddonDetails,
|
||||
HassioAddonSetOptionParams,
|
||||
HassioAddonSetSecurityParams,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
@@ -61,26 +61,33 @@ import {
|
||||
stopHassioAddon,
|
||||
uninstallHassioAddon,
|
||||
validateHassioAddonOption,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import type { HassioStats } from "../../../../../data/hassio/common";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import type { HassioStats } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
} from "../../../../../data/hassio/common";
|
||||
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import type {
|
||||
StoreAddon,
|
||||
StoreAddonDetails,
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { mdiHomeAssistant } from "../../../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bytesToString } from "../../../../../util/bytes-to-string";
|
||||
import "../../components/supervisor-apps-card-content";
|
||||
import "../components/supervisor-app-metric";
|
||||
import { extractChangelog } from "../util/supervisor-app";
|
||||
import "./supervisor-app-system-managed";
|
||||
import "../components/supervisor-app-update-available-card";
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../src/types";
|
||||
import { bytesToString } from "../../../../src/util/bytes-to-string";
|
||||
import "../../components/hassio-card-content";
|
||||
import "../../components/supervisor-metric";
|
||||
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
|
||||
import { showSystemManagedDialog } from "../../dialogs/system-managed/show-dialog-system-managed";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../../update-available/update-available-card";
|
||||
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
|
||||
import "./hassio-addon-system-managed";
|
||||
|
||||
const STAGE_ICON = {
|
||||
stable: mdiCheckCircle,
|
||||
@@ -99,8 +106,8 @@ const RATING_ICON = {
|
||||
8: mdiNumeric8,
|
||||
};
|
||||
|
||||
@customElement("supervisor-app-info")
|
||||
class SupervisorAppInfo extends LitElement {
|
||||
@customElement("hassio-addon-info")
|
||||
class HassioAddonInfo extends LitElement {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
@@ -111,6 +118,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean, attribute: "control-enabled" })
|
||||
public controlEnabled = false;
|
||||
|
||||
@@ -120,27 +129,32 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearTimeout(this._fetchDataTimeout);
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const addonStoreInfo =
|
||||
!this.addon.detached && !this.addon.available
|
||||
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
|
||||
: undefined;
|
||||
const metrics = [
|
||||
{
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.cpu_usage"
|
||||
),
|
||||
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.ram_usage"
|
||||
),
|
||||
description: this.supervisor.localize("addon.dashboard.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
@@ -153,32 +167,33 @@ class SupervisorAppInfo extends LitElement {
|
||||
return html`
|
||||
${this.addon.update_available
|
||||
? html`
|
||||
<supervisor-app-update-available-card
|
||||
<update-available-card
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.addonSlug=${this.addon.slug}
|
||||
@update-complete=${this._updateComplete}
|
||||
></supervisor-app-update-available-card>
|
||||
></update-available-card>
|
||||
`
|
||||
: nothing}
|
||||
${"protected" in this.addon && !this.addon.protected
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.protection_mode.title"
|
||||
.title=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.protection_mode.content"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.content"
|
||||
)}
|
||||
<ha-button
|
||||
variant="danger"
|
||||
slot="action"
|
||||
@click=${this._protectionToggled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.protection_mode.enable"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.enable"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
@@ -186,11 +201,11 @@ class SupervisorAppInfo extends LitElement {
|
||||
: nothing}
|
||||
${systemManaged
|
||||
? html`
|
||||
<supervisor-app-system-managed
|
||||
.hass=${this.hass}
|
||||
<hassio-addon-system-managed
|
||||
.supervisor=${this.supervisor}
|
||||
.narrow=${this.narrow}
|
||||
.hideButton=${this.controlEnabled}
|
||||
></supervisor-app-system-managed>
|
||||
></hassio-addon-system-managed>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -204,8 +219,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
${this._computeIsRunning
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.app_running"
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
class="running"
|
||||
.path=${mdiPlayCircle}
|
||||
@@ -213,8 +228,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.app_stopped"
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_stopped"
|
||||
)}
|
||||
class="stopped"
|
||||
.path=${mdiCircleOffOutline}
|
||||
@@ -227,21 +242,21 @@ class SupervisorAppInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.current_version",
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.current_version",
|
||||
{ version: this.addon.version }
|
||||
)}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>
|
||||
`
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>`}
|
||||
</div>
|
||||
@@ -258,8 +273,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="stage"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${this.addon.stage}`
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.stages.${this.addon.stage}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -277,13 +292,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
class=${classMap({
|
||||
green: Number(this.addon.rating) >= 6,
|
||||
yellow: [3, 4, 5].includes(Number(this.addon.rating)),
|
||||
red: Number(this.addon.rating) <= 2,
|
||||
red: Number(this.addon.rating) >= 2,
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="rating"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.rating"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.rating"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -297,8 +312,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="host_network"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.host"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -313,8 +328,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="full_access"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.hardware"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hardware"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -329,8 +344,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="homeassistant_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.core"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.core"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -348,8 +363,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="hassio_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
) || this.addon.hassio_role
|
||||
)}
|
||||
>
|
||||
@@ -367,8 +382,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="docker_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.docker"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.docker"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -383,8 +398,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="host_pid"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.host_pid"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host_pid"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -400,8 +415,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
class=${this._computeApparmorClassName}
|
||||
id="apparmor"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.apparmor"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.apparmor"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -416,8 +431,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="auth_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.auth"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.auth"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -432,8 +447,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="ingress"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.ingress"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.ingress"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -451,8 +466,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._showMoreInfo}
|
||||
id="signed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.capability.label.signed"
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.signed"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -464,12 +479,10 @@ class SupervisorAppInfo extends LitElement {
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
filled
|
||||
@click=${this._showSystemManagedInfo}
|
||||
@click=${this._showSystemManagedDialog}
|
||||
id="system_managed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.badge"
|
||||
)
|
||||
this.supervisor.localize("addon.system_managed.badge")
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -483,17 +496,14 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.visit_app_page",
|
||||
{
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.addon.name}</a
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.addon.name}</a
|
||||
>`,
|
||||
})}
|
||||
</div>
|
||||
<div class="addon-container">
|
||||
<div>
|
||||
@@ -516,13 +526,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
>
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.boot.title"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.boot.description"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -537,13 +547,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.watchdog.title"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.watchdog.description"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -561,13 +571,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.auto_update.title"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.auto_update.description"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -584,13 +594,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.ingress_panel.title"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.ingress_panel.description"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -607,13 +617,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
? html`
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.protected.title"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.option.protected.description"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -634,19 +644,17 @@ class SupervisorAppInfo extends LitElement {
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.hostname"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.hostname")}
|
||||
</span>
|
||||
<code slot="description"> ${this.addon.hostname} </code>
|
||||
</ha-settings-row>
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-app-metric
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-app-metric>
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}`
|
||||
: nothing}
|
||||
@@ -655,6 +663,30 @@ class SupervisorAppInfo extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${!this.addon.version && addonStoreInfo && !this.addon.available
|
||||
? !addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this.addon.arch
|
||||
)
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo!.homeassistant,
|
||||
}
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<div>
|
||||
@@ -667,19 +699,14 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._stopClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.stop"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.stop")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._restartClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.restart"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.restart")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: html`
|
||||
@@ -688,9 +715,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
.progress=${this.addon.state === "startup"}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.start"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.start")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -704,9 +729,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
@click=${this._uninstallClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.uninstall"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.uninstall")}
|
||||
</ha-progress-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
@@ -715,9 +738,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this._rebuildClicked}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.rebuild"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.rebuild")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -739,8 +760,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
? this._openIngress
|
||||
: undefined}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.open_web_ui"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.open_web_ui"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
@@ -751,9 +772,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
.disabled=${!this.addon.available}
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.install"
|
||||
)}
|
||||
${this.supervisor.localize("addon.dashboard.install")}
|
||||
</ha-progress-button>
|
||||
`}
|
||||
</div>
|
||||
@@ -785,7 +804,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
"state" in this.addon &&
|
||||
this.addon.state === "startup"
|
||||
) {
|
||||
// App is starting up, wait for it to start
|
||||
// Addon is starting up, wait for it to start
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}
|
||||
@@ -838,24 +857,28 @@ class SupervisorAppInfo extends LitElement {
|
||||
|
||||
private _showMoreInfo(ev): void {
|
||||
const id = ev.currentTarget.id as AddonCapability;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.${id}.title`
|
||||
),
|
||||
text: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.${id}.description`
|
||||
),
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
|
||||
content:
|
||||
id === "stage"
|
||||
? this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`,
|
||||
{
|
||||
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
|
||||
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
|
||||
icon_deprecated: `<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`,
|
||||
}
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _showSystemManagedInfo() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.description"
|
||||
),
|
||||
private _showSystemManagedDialog() {
|
||||
showSystemManagedDialog(this, {
|
||||
addon: this.addon as HassioAddonDetails,
|
||||
supervisor: this.supervisor,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -879,7 +902,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _openIngress(): void {
|
||||
navigate(`/app/${this.addon.slug}`);
|
||||
navigate(`/hassio/ingress/${this.addon.slug}`);
|
||||
}
|
||||
|
||||
private get _computeShowIngressUI(): boolean {
|
||||
@@ -913,12 +936,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,12 +956,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,12 +976,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,12 +996,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,12 +1016,9 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.failed_to_save",
|
||||
{
|
||||
error: extractApiErrorMessage(err),
|
||||
}
|
||||
);
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,19 +1029,14 @@ class SupervisorAppInfo extends LitElement {
|
||||
this.addon.slug
|
||||
);
|
||||
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.apps.dashboard.changelog"),
|
||||
text: html`<ha-markdown
|
||||
.content=${extractChangelog(
|
||||
this.addon as HassioAddonDetails,
|
||||
content
|
||||
)}
|
||||
></ha-markdown>`,
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
content: extractChangelog(this.addon as HassioAddonDetails, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.get_changelog"
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.get_changelog"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
@@ -1063,9 +1066,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.install"
|
||||
),
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.install"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1090,9 +1091,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.stop"
|
||||
),
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1100,10 +1099,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _restartClicked(ev: CustomEvent): Promise<void> {
|
||||
if (this._isSystemManaged(this.addon) && !this.controlEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
@@ -1112,14 +1107,12 @@ class SupervisorAppInfo extends LitElement {
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "restart",
|
||||
path: "stop",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.restart"
|
||||
),
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1134,9 +1127,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
await rebuildLocalAddon(this.hass, this.addon.slug);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.rebuild"
|
||||
),
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1153,15 +1144,15 @@ class SupervisorAppInfo extends LitElement {
|
||||
);
|
||||
if (!validate.valid) {
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.start_invalid_config"
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.start_invalid_config"
|
||||
),
|
||||
text: validate.message.split(" Got ")[0],
|
||||
confirm: () => this._openConfiguration(),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.go_to_config"
|
||||
confirmText: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.go_to_config"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
@@ -1171,9 +1162,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.validate_config"
|
||||
),
|
||||
title: "Failed to validate addon configuration",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1192,9 +1181,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.start"
|
||||
),
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.start"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1204,7 +1191,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _openConfiguration(): void {
|
||||
navigate(`/config/app/${this.addon.slug}/config`);
|
||||
navigate(`/hassio/addon/${this.addon.slug}/config`);
|
||||
}
|
||||
|
||||
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
|
||||
@@ -1220,18 +1207,13 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.uninstall_dialog.title",
|
||||
{
|
||||
name: this.addon.name,
|
||||
}
|
||||
),
|
||||
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
||||
name: this.addon.name,
|
||||
}),
|
||||
text: html`
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.uninstall_dialog.remove_data"
|
||||
)}
|
||||
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
|
||||
</p>`}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -1241,10 +1223,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`,
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.uninstall_dialog.uninstall"
|
||||
),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
@@ -1265,8 +1245,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.uninstall"
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.uninstall"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
@@ -1283,6 +1263,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -1312,8 +1293,8 @@ class SupervisorAppInfo extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.addon-header {
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
@@ -1403,11 +1384,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
--markdown-image-background-color: transparent;
|
||||
--markdown-image-border-radius: 0;
|
||||
--markdown-image-min-height: auto;
|
||||
--markdown-image-text-indent: 0;
|
||||
--markdown-image-transition: none;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
@@ -1449,7 +1425,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
supervisor-app-update-available-card {
|
||||
update-available-card {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -1467,6 +1443,6 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-info": SupervisorAppInfo;
|
||||
"hassio-addon-info": HassioAddonInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
@customElement("supervisor-app-system-managed")
|
||||
class SupervisorAppSystemManaged extends LitElement {
|
||||
@customElement("hassio-addon-system-managed")
|
||||
class HassioAddonSystemManaged extends LitElement {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-button" }) public hideButton =
|
||||
false;
|
||||
@@ -19,20 +19,14 @@ class SupervisorAppSystemManaged extends LitElement {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.title"
|
||||
)}
|
||||
.title=${this.supervisor.localize("addon.system_managed.title")}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.description"
|
||||
)}
|
||||
${this.supervisor.localize("addon.system_managed.description")}
|
||||
${!this.hideButton
|
||||
? html`
|
||||
<ha-button slot="action" @click=${this._takeControl}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.system_managed.take_control"
|
||||
)}
|
||||
${this.supervisor.localize("addon.system_managed.take_control")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -56,7 +50,7 @@ class SupervisorAppSystemManaged extends LitElement {
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-system-managed": SupervisorAppSystemManaged;
|
||||
"hassio-addon-system-managed": HassioAddonSystemManaged;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
@@ -6,19 +6,22 @@ import {
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import "../../../logs/error-log-card";
|
||||
import "../../../../../components/search-input";
|
||||
import { extractSearchParam } from "../../../../../common/url/search-params";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import "../../../../src/panels/config/logs/error-log-card";
|
||||
import "../../../../src/components/search-input";
|
||||
import { extractSearchParam } from "../../../../src/common/url/search-params";
|
||||
|
||||
@customElement("supervisor-app-log-tab")
|
||||
class SupervisorAppLogDashboard extends LitElement {
|
||||
@customElement("hassio-addon-log-tab")
|
||||
class HassioAddonLogDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _filter = extractSearchParam("filter") || "";
|
||||
@@ -33,12 +36,13 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
.label=${this.supervisor.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.header=${this.addon.name}
|
||||
.provider=${this.addon.slug}
|
||||
.filter=${this._filter}
|
||||
@@ -55,11 +59,11 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
padding: 8px;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
@@ -83,6 +87,6 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-log-tab": SupervisorAppLogDashboard;
|
||||
"hassio-addon-log-tab": HassioAddonLogDashboard;
|
||||
}
|
||||
}
|
||||
425
hassio/src/backups/hassio-backups.ts
Normal file
425
hassio/src/backups/hassio-backups.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { relativeTime } from "../../../src/common/datetime/relative_time";
|
||||
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../src/components/data-table/ha-data-table";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-fab";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import {
|
||||
fetchHassioBackups,
|
||||
friendlyFolderName,
|
||||
reloadHassioBackups,
|
||||
removeBackup,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
|
||||
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
|
||||
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
type BackupItem = HassioBackup & {
|
||||
secondary: string;
|
||||
};
|
||||
|
||||
@customElement("hassio-backups")
|
||||
export class HassioBackups extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _selectedBackups: string[] = [];
|
||||
|
||||
@state() private _backups?: HassioBackup[] = [];
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||
if (backup.type === "full") {
|
||||
return this.supervisor.localize("backup.full_backup");
|
||||
}
|
||||
const content: string[] = [];
|
||||
if (backup.content.homeassistant) {
|
||||
content.push("Home Assistant");
|
||||
}
|
||||
if (backup.content.folders.length !== 0) {
|
||||
for (const folder of backup.content.folders) {
|
||||
content.push(friendlyFolderName[folder] || folder);
|
||||
}
|
||||
}
|
||||
|
||||
if (backup.content.addons.length !== 0) {
|
||||
for (const addon of backup.content.addons) {
|
||||
content.push(
|
||||
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
|
||||
?.name || addon
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return content.join(", ");
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass && this.isConnected) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
this._firstUpdatedCalled = true;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
|
||||
name: {
|
||||
title: this.supervisor.localize("backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
template: (backup) =>
|
||||
html`${backup.name || backup.slug}
|
||||
<div class="secondary">${backup.secondary}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: this.supervisor.localize("backup.size"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
backup.location || this.supervisor.localize("backup.data_disk"),
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
secondary: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
|
||||
backups.map((backup) => ({
|
||||
...backup,
|
||||
secondary: this._computeBackupContent(backup),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._isLoading) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this.supervisor.localize("backup.loading_backups")}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("backup.search")}
|
||||
.noDataText=${this.supervisor.localize("backup.no_backups")}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._backupData(this._backups || [])}
|
||||
id="slug"
|
||||
@row-click=${this._handleRowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
selectable
|
||||
has-fab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor?.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("common.reload")}
|
||||
</ha-list-item>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("dialog.backup_location.title")}
|
||||
</ha-list-item>
|
||||
${atLeastVersion(this.hass.config.version, 0, 116)
|
||||
? html`<ha-list-item>
|
||||
${this.supervisor.localize("backup.upload_backup")}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
|
||||
${this._selectedBackups.length
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.supervisor.localize("backup.selected", {
|
||||
number: this._selectedBackups.length,
|
||||
})}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
@click=${this._deleteSelected}
|
||||
>
|
||||
${this.supervisor.localize("backup.delete_selected")}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize(
|
||||
"backup.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</div>
|
||||
</div> `
|
||||
: ""}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
@click=${this._createBackup}
|
||||
.label=${this.supervisor.localize("backup.create_backup")}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._fetchBackups();
|
||||
break;
|
||||
case 1:
|
||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||
break;
|
||||
case 2:
|
||||
this._showUploadBackupDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedBackups = ev.detail.value;
|
||||
}
|
||||
|
||||
private _showUploadBackupDialog() {
|
||||
showBackupUploadDialog(this, {
|
||||
showBackup: (slug: string) =>
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
}),
|
||||
reloadBackup: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _fetchBackups() {
|
||||
this._isLoading = true;
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
this._isLoading = false;
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("backup.delete_backup_title"),
|
||||
text: this.supervisor.localize("backup.delete_backup_text", {
|
||||
number: this._selectedBackups.length,
|
||||
}),
|
||||
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("backup.failed_to_delete"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this._fetchBackups();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const slug = ev.detail.id;
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private _createBackup() {
|
||||
if (this.supervisor!.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor!.localize("backup.could_not_create"),
|
||||
text: this.supervisor!.localize("backup.create_blocked_not_running", {
|
||||
state: this.supervisor!.info.state,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioCreateBackupDialog(this, {
|
||||
supervisor: this.supervisor!,
|
||||
onCreate: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 58px;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: initial;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.header-toolbar .selected-txt {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-backups": HassioBackups;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("supervisor-apps-card-content")
|
||||
class SupervisorAppsCardContent extends LitElement {
|
||||
@customElement("hassio-card-content")
|
||||
class HassioCardContent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@@ -24,7 +24,7 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public iconClass?: string;
|
||||
|
||||
@property() public icon = mdiHelpCircleOutline;
|
||||
@property() public icon = mdiHelpCircle;
|
||||
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
|
||||
@@ -70,9 +70,9 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
margin-right: var(--ha-space-6);
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-3);
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@@ -106,8 +106,8 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
margin-top: var(--ha-space-1);
|
||||
margin-right: var(--ha-space-4);
|
||||
margin-top: 4px;
|
||||
margin-right: 16px;
|
||||
float: left;
|
||||
}
|
||||
.icon_image.stopped,
|
||||
@@ -119,8 +119,8 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
background-color: var(--warning-color);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: var(--ha-space-2);
|
||||
right: var(--ha-space-2);
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
.topbar {
|
||||
@@ -146,6 +146,6 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-apps-card-content": SupervisorAppsCardContent;
|
||||
"hassio-card-content": HassioCardContent;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { StoreAddon } from "../../../../data/supervisor/store";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
89
hassio/src/components/hassio-upload-backup.ts
Normal file
89
hassio/src/components/hassio-upload-backup.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import { uploadBackup } from "../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hassio-upload-backup")
|
||||
export class HassioUploadBackup extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() public value: string | null = null;
|
||||
|
||||
@state() private _uploading = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept="application/x-tar"
|
||||
.label=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}
|
||||
.supports=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_supports"
|
||||
) || "Supports .TAR files"}
|
||||
.secondary=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_drop"
|
||||
) || "Or drop your file here"}
|
||||
@file-picked=${this._uploadFile}
|
||||
@files-cleared=${this._clear}
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this.value = null;
|
||||
fireEvent(this, "backup-cleared");
|
||||
}
|
||||
|
||||
private async _uploadFile(ev) {
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (!["application/x-tar"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
text: "Please choose a Home Assistant backup file (.tar)",
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
text: extractApiErrorMessage(err),
|
||||
confirmText: "ok",
|
||||
});
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-upload-backup": HassioUploadBackup;
|
||||
}
|
||||
}
|
||||
460
hassio/src/components/supervisor-backup-content.ts
Normal file
460
hassio/src/components/supervisor-backup-content.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { mdiFolder, mdiPuzzle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-password-field";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import type {
|
||||
HassioBackupDetail,
|
||||
HassioFullBackupCreateParams,
|
||||
HassioPartialBackupCreateParams,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AddonCheckboxItem extends CheckboxItem {
|
||||
version: string;
|
||||
}
|
||||
|
||||
const _computeFolders = (folders): CheckboxItem[] => {
|
||||
const list: CheckboxItem[] = [];
|
||||
if (folders.includes("ssl")) {
|
||||
list.push({ slug: "ssl", name: "SSL", checked: false });
|
||||
}
|
||||
if (folders.includes("share")) {
|
||||
list.push({ slug: "share", name: "Share", checked: false });
|
||||
}
|
||||
if (folders.includes("media")) {
|
||||
list.push({ slug: "media", name: "Media", checked: false });
|
||||
}
|
||||
if (folders.includes("addons/local")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
||||
}
|
||||
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
};
|
||||
|
||||
const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: false,
|
||||
}))
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
@customElement("supervisor-backup-content")
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
|
||||
@property({ attribute: false })
|
||||
public backupType: HassioBackupDetail["type"] = "full";
|
||||
|
||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public homeAssistant = false;
|
||||
|
||||
@property({ attribute: false }) public backupHasPassword = false;
|
||||
|
||||
@property({ type: Boolean }) public onboarding = false;
|
||||
|
||||
@property({ attribute: false }) public backupName = "";
|
||||
|
||||
@property({ attribute: false }) public backupPassword = "";
|
||||
|
||||
@property({ attribute: false }) public confirmBackupPassword = "";
|
||||
|
||||
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this.folders = _computeFolders(
|
||||
this.backup
|
||||
? this.backup.folders
|
||||
: ["ssl", "share", "media", "addons/local"]
|
||||
);
|
||||
this.addons = _computeAddons(
|
||||
this.backup ? this.backup.addons : this.supervisor?.addon.addons
|
||||
);
|
||||
this.backupType = this.backup?.type || "full";
|
||||
this.backupName = this.backup?.name || "";
|
||||
this.backupHasPassword = this.backup?.protected || false;
|
||||
}
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
const foldersSection =
|
||||
this.backupType === "partial" ? this._getSection("folders") : undefined;
|
||||
const addonsSection =
|
||||
this.backupType === "partial" ? this._getSection("addons") : undefined;
|
||||
|
||||
return html`
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
new Date(this.backup.date),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: this.backup.date}
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>`}
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
name="backupType"
|
||||
.checked=${this.backupType === "full"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
name="backupType"
|
||||
.checked=${this.backupType === "partial"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.backupType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
${!this.backup || this.backup.homeassistant
|
||||
? html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass?.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.onboarding || this.homeAssistant}
|
||||
.disabled=${this.onboarding}
|
||||
@change=${this._toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${foldersSection.checked}
|
||||
.indeterminate=${foldersSection.indeterminate}
|
||||
.section=${"folders"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${foldersSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
${addonsSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${addonsSection.checked}
|
||||
.indeterminate=${addonsSection.indeterminate}
|
||||
.section=${"addons"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${addonsSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
</div> `
|
||||
: ""}
|
||||
${this.backupType === "partial" &&
|
||||
(!this.backup || this.backupHasPassword)
|
||||
? html`<hr />`
|
||||
: ""}
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@change=${this._toggleHasPassword}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleHomeAssistant() {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.partial-picker ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.partial-picker ha-checkbox {
|
||||
--mdc-checkbox-touch-target-size: 32px;
|
||||
}
|
||||
.partial-picker {
|
||||
display: block;
|
||||
margin: 0px -6px;
|
||||
}
|
||||
supervisor-formfield-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 30px;
|
||||
margin-inline-start: 30px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
ha-formfield.password {
|
||||
display: block;
|
||||
margin: 0 -14px -16px;
|
||||
}
|
||||
.backup-types {
|
||||
display: flex;
|
||||
margin-left: -13px;
|
||||
margin-inline-start: -13px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.sub-header {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
public backupDetails():
|
||||
| HassioPartialBackupCreateParams
|
||||
| HassioFullBackupCreateParams {
|
||||
const data: any = {};
|
||||
|
||||
if (!this.backup && this.hass) {
|
||||
data.name =
|
||||
this.backupName ||
|
||||
formatDate(new Date(), this.hass.locale, this.hass.config);
|
||||
}
|
||||
|
||||
if (this.backupHasPassword) {
|
||||
data.password = this.backupPassword;
|
||||
if (!this.backup) {
|
||||
data.confirm_password = this.confirmBackupPassword;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.backupType === "full") {
|
||||
return data;
|
||||
}
|
||||
|
||||
const addons = this.addons
|
||||
?.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this.folders
|
||||
?.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
if (addons?.length) {
|
||||
data.addons = addons;
|
||||
}
|
||||
if (folders?.length) {
|
||||
data.folders = folders;
|
||||
}
|
||||
|
||||
// onboarding needs at least homeassistant to restore
|
||||
data.homeassistant = this.onboarding || this.homeAssistant;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private _getSection(section: string) {
|
||||
const templates: TemplateResult[] = [];
|
||||
const addons =
|
||||
section === "addons"
|
||||
? new Map(
|
||||
this.supervisor?.addon.addons.map((item) => [item.slug, item])
|
||||
)
|
||||
: undefined;
|
||||
let checkedItems = 0;
|
||||
this[section].forEach((item) => {
|
||||
templates.push(
|
||||
html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${item.name}
|
||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
||||
.imageUrl=${section === "addons" &&
|
||||
!this.onboarding &&
|
||||
this.hass &&
|
||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
addons?.get(item.slug)?.icon
|
||||
? `/api/hassio/addons/${item.slug}/icon`
|
||||
: undefined}
|
||||
.version=${item.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.item=${item}
|
||||
.checked=${item.checked}
|
||||
.section=${section}
|
||||
@change=${this._updateSectionEntry}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
);
|
||||
|
||||
if (item.checked) {
|
||||
checkedItems++;
|
||||
}
|
||||
});
|
||||
|
||||
const checked = checkedItems === this[section].length;
|
||||
|
||||
return {
|
||||
templates,
|
||||
checked,
|
||||
indeterminate: !checked && checkedItems !== 0,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this[input.name] = input.value;
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: InputEvent) {
|
||||
const input = ev.currentTarget as HaTextField;
|
||||
this[input.name!] = input.value;
|
||||
}
|
||||
|
||||
private _toggleHasPassword(): void {
|
||||
this.backupHasPassword = !this.backupHasPassword;
|
||||
}
|
||||
|
||||
private _toggleSection(ev): void {
|
||||
const section = ev.currentTarget.section;
|
||||
|
||||
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
checked: ev.currentTarget.checked,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSectionEntry(ev): void {
|
||||
const item = ev.currentTarget.item;
|
||||
const section = ev.currentTarget.section;
|
||||
this[section] = this[section].map((entry) =>
|
||||
entry.slug === item.slug
|
||||
? {
|
||||
...entry,
|
||||
checked: ev.currentTarget.checked,
|
||||
}
|
||||
: entry
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-backup-content": SupervisorBackupContent;
|
||||
}
|
||||
}
|
||||
60
hassio/src/components/supervisor-formfield-label.ts
Normal file
60
hassio/src/components/supervisor-formfield-label.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
|
||||
@customElement("supervisor-formfield-label")
|
||||
class SupervisorFormfieldLabel extends LitElement {
|
||||
@property({ type: String }) public label!: string;
|
||||
|
||||
@property({ attribute: false }) public imageUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public iconPath?: string;
|
||||
|
||||
@property({ type: String }) public version?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.imageUrl
|
||||
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
|
||||
: this.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
class="icon"
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
<span class="label">${this.label}</span>
|
||||
${this.version
|
||||
? html`<span class="version">(${this.version})</span>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.version {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
max-height: 22px;
|
||||
max-width: 22px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-formfield-label": SupervisorFormfieldLabel;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../../../components/ha-bar";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import { roundWithOneDecimal } from "../../../../../util/calculate";
|
||||
import "../../../src/components/ha-bar";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { roundWithOneDecimal } from "../../../src/util/calculate";
|
||||
|
||||
@customElement("supervisor-app-metric")
|
||||
class SupervisorAppMetric extends LitElement {
|
||||
@customElement("supervisor-metric")
|
||||
class SupervisorMetric extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
@property({ type: String }) public description!: string;
|
||||
@@ -70,6 +70,6 @@ class SupervisorAppMetric extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-app-metric": SupervisorAppMetric;
|
||||
"supervisor-metric": SupervisorMetric;
|
||||
}
|
||||
}
|
||||
164
hassio/src/dashboard/hassio-addons.ts
Normal file
164
hassio/src/dashboard/hassio-addons.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/search-input";
|
||||
import type { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addons")
|
||||
class HassioAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
suffix
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.supervisor.localize("dashboard.search_addons")}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.addon.addons.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<button class="link" @click=${this._openStore}>
|
||||
${this.supervisor.localize("dashboard.no_addons")}
|
||||
</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: this._getAddons(this.supervisor.addon.addons, this._filter).map(
|
||||
(addon) => html`
|
||||
<ha-card outlined .addon=${addon} @click=${this._addonTapped}>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
available
|
||||
.showTopbar=${addon.update_available}
|
||||
topbarClass="update"
|
||||
.icon=${addon.update_available!
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.supervisor.localize("dashboard.addon_stopped")
|
||||
: addon.update_available!
|
||||
? this.supervisor.localize(
|
||||
"dashboard.addon_new_version"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getAddons = memoizeOne(
|
||||
(addons: HassioAddonInfo[], filter?: string) => {
|
||||
if (filter) {
|
||||
addons = addons.filter((addon) => {
|
||||
const lowerCaseFilter = filter.toLowerCase();
|
||||
return (
|
||||
addon.name.toLowerCase().includes(lowerCaseFilter) ||
|
||||
addon.description.toLowerCase().includes(lowerCaseFilter) ||
|
||||
addon.slug.toLowerCase().includes(lowerCaseFilter)
|
||||
);
|
||||
});
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
direction: ltr;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
.content {
|
||||
margin-bottom: 72px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _addonTapped(ev: any): void {
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||
}
|
||||
|
||||
private _openStore(): void {
|
||||
navigate("/hassio/store");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addons": HassioAddons;
|
||||
}
|
||||
}
|
||||
150
hassio/src/dashboard/hassio-dashboard.ts
Normal file
150
hassio/src/dashboard/hassio-dashboard.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-fab";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
firstUpdated() {
|
||||
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
import("./hassio-update");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@click=${this._handleCheckUpdates}
|
||||
.path=${mdiRefresh}
|
||||
.label=${this.supervisor.localize("store.check_updates")}
|
||||
></ha-icon-button>
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-addons>
|
||||
<a href="/hassio/store">
|
||||
<ha-fab
|
||||
.label=${this.supervisor.localize("panel.store")}
|
||||
extended
|
||||
class="non-tabs"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon></ha-fab
|
||||
></a>
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
has-fab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
`
|
||||
: ""}
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
|
||||
<a href="/hassio/store" slot="fab">
|
||||
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></a>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleCheckUpdates() {
|
||||
try {
|
||||
await reloadHassioAddons(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-fab.non-tabs {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right));
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom));
|
||||
inset-inline-end: calc(16px + var(--safe-area-inset-right));
|
||||
inset-inline-start: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-dashboard": HassioDashboard;
|
||||
}
|
||||
}
|
||||
158
hassio/src/dashboard/hassio-update.ts
Normal file
158
hassio/src/dashboard/hassio-update.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import type {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const computeVersion = (key: string, version: string): string =>
|
||||
key === "os" ? version : `${key}-${version}`;
|
||||
|
||||
@customElement("hassio-update")
|
||||
export class HassioUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
private _pendingUpdates = memoizeOne(
|
||||
(supervisor: Supervisor): number =>
|
||||
Object.keys(supervisor).filter(
|
||||
(value) => supervisor[value].update_available
|
||||
).length
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const updatesAvailable = this._pendingUpdates(this.supervisor);
|
||||
if (!updatesAvailable) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
${this.supervisor.localize("common.update_available", {
|
||||
count: updatesAvailable,
|
||||
})}
|
||||
🎉
|
||||
</h1>
|
||||
<div class="card-group">
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
"core",
|
||||
this.supervisor.core
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
"supervisor",
|
||||
this.supervisor.supervisor
|
||||
)}
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
"os",
|
||||
this.supervisor.os
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUpdateCard(
|
||||
name: string,
|
||||
key: string,
|
||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
|
||||
) {
|
||||
if (!object.update_available) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="update-heading">${name}</div>
|
||||
<ha-settings-row two-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${computeVersion(key, object.version!)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row two-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${computeVersion(key, object.version_latest!)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" href="/hassio/update-available/${key}">
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.icon {
|
||||
--mdc-icon-size: 48px;
|
||||
float: right;
|
||||
margin: 0 0 2px 10px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.update-heading {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-content {
|
||||
height: calc(100% - 47px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-update": HassioUpdate;
|
||||
}
|
||||
}
|
||||
155
hassio/src/dialogs/backup/dialog-hassio-backup-location.ts
Normal file
155
hassio/src/dialogs/backup/dialog-hassio-backup-location.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { changeMountOptions } from "../../../../src/data/supervisor/mounts";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location";
|
||||
|
||||
const SCHEMA = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "default_backup_mount",
|
||||
required: true,
|
||||
selector: { backup_location: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
@customElement("dialog-hassio-backup-location")
|
||||
class HassioBackupLocationDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupLocationDialogParams;
|
||||
|
||||
@state() private _data?: { default_backup_mount: string | null };
|
||||
|
||||
@state() private _waiting?: boolean;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: HassioBackupLocationDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._waiting = undefined;
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this._dialogParams.supervisor.localize(
|
||||
"dialog.backup_location.title"
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${SCHEMA()}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
// @ts-ignore
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this._dialogParams!.supervisor.localize(
|
||||
`dialog.backup_location.options.${schema.name}.name`
|
||||
) || schema.name;
|
||||
|
||||
private _computeHelperCallback = (
|
||||
// @ts-ignore
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this._dialogParams!.supervisor.localize(
|
||||
`dialog.backup_location.options.${schema.name}.description`
|
||||
);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const newLocation = ev.detail.value.default_backup_mount;
|
||||
this._data = {
|
||||
default_backup_mount: newLocation === "/backup" ? null : newLocation,
|
||||
};
|
||||
}
|
||||
|
||||
private async _changeMount() {
|
||||
if (!this._data) {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._waiting = true;
|
||||
try {
|
||||
await changeMountOptions(this.hass, this._data);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._waiting = false;
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.delete-btn {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup-location": HassioBackupLocationDialog;
|
||||
}
|
||||
}
|
||||
113
hassio/src/dialogs/backup/dialog-hassio-backup-upload.ts
Normal file
113
hassio/src/dialogs/backup/dialog-hassio-backup-upload.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-upload-backup";
|
||||
import type { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload";
|
||||
|
||||
@customElement("dialog-hassio-backup-upload")
|
||||
export class DialogHassioBackupUpload
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupUploadDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupUploadDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: HassioBackupUploadDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._dialogParams && !this._dialogParams.onboarding) {
|
||||
if (this._dialogParams.reloadBackup) {
|
||||
this._dialogParams.reloadBackup();
|
||||
}
|
||||
}
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title"
|
||||
>${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass?.localize("ui.common.close") || "Close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
dialogInitialFocus
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._dialogParams?.showBackup(backup.slug);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup-upload": DialogHassioBackupUpload;
|
||||
}
|
||||
}
|
||||
339
hassio/src/dialogs/backup/dialog-hassio-backup.ts
Normal file
339
hassio/src/dialogs/backup/dialog-hassio-backup.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
|
||||
import { slugify } from "../../../../src/common/string/slugify";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-dialog-header";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import type { HassioBackupDetail } from "../../../../src/data/hassio/backup";
|
||||
import {
|
||||
fetchHassioBackupInfo,
|
||||
removeBackup,
|
||||
restoreBackup,
|
||||
} from "../../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _backup?: HassioBackupDetail;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupDialogParams;
|
||||
|
||||
@state() private _restoringBackup = false;
|
||||
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(dialogParams: HassioBackupDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._backup = undefined;
|
||||
this._dialogParams = undefined;
|
||||
this._restoringBackup = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
.disableCancelAction=${!this._error}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${this._backup.name}
|
||||
>${this._backup.name}</span
|
||||
>
|
||||
${!this._dialogParams.onboarding && this._dialogParams.supervisor
|
||||
? html`<ha-button-menu
|
||||
slot="actionItems"
|
||||
fixed
|
||||
@action=${this._handleMenuAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"backup.more_actions"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
>${this._dialogParams.supervisor.localize(
|
||||
"backup.download_backup"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="error"
|
||||
>${this._dialogParams.supervisor.localize(
|
||||
"backup.delete_backup_title"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._restoringBackup
|
||||
? html`<div class="loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`
|
||||
: html`
|
||||
<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
`}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._downloadClicked();
|
||||
break;
|
||||
case 1:
|
||||
this._deleteClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _restoreClicked() {
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._restoringBackup = true;
|
||||
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (supervisor !== undefined && supervisor.info.state !== "running") {
|
||||
await showAlertDialog(this, {
|
||||
title: supervisor.localize("backup.could_not_restore"),
|
||||
text: supervisor.localize("backup.restore_blocked_not_running", {
|
||||
state: supervisor.info.state,
|
||||
}),
|
||||
});
|
||||
this._restoringBackup = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
),
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restoreBackup(
|
||||
this.hass,
|
||||
this._backup!.type,
|
||||
this._backup!.slug,
|
||||
{ ...backupDetails, background: this._dialogParams?.onboarding },
|
||||
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
);
|
||||
|
||||
this._dialogParams?.onRestoring?.();
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteClicked() {
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (!supervisor) return;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor!.localize("backup.confirm_delete_title"),
|
||||
text: supervisor!.localize("backup.confirm_delete_text"),
|
||||
confirmText: supervisor!.localize("backup.delete"),
|
||||
dismissText: supervisor!.localize("backup.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeBackup(this.hass!, this._backup!.slug);
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.body.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _downloadClicked() {
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (!supervisor) return;
|
||||
|
||||
let signedPath: { path: string };
|
||||
try {
|
||||
signedPath = await getSignedPath(
|
||||
this.hass!,
|
||||
`/api/hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/download`
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.href.includes("ui.nabu.casa")) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fileDownload(
|
||||
signedPath.path,
|
||||
`home_assistant_backup_${slugify(this._computeName)}.tar`
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.loading {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup": HassioBackupDialog;
|
||||
}
|
||||
}
|
||||
158
hassio/src/dialogs/backup/dialog-hassio-create-backup.ts
Normal file
158
hassio/src/dialogs/backup/dialog-hassio-create-backup.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import {
|
||||
createHassioFullBackup,
|
||||
createHassioPartialBackup,
|
||||
} from "../../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup";
|
||||
|
||||
@customElement("dialog-hassio-create-backup")
|
||||
class HassioCreateBackupDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioCreateBackupDialogParams;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _creatingBackup = false;
|
||||
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
this._creatingBackup = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
this._creatingBackup = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams.supervisor.localize("backup.create_backup")
|
||||
)}
|
||||
>
|
||||
${this._creatingBackup
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.close")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._creatingBackup}
|
||||
slot="primaryAction"
|
||||
@click=${this._createBackup}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("backup.create")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _createBackup(): Promise<void> {
|
||||
if (this._dialogParams!.supervisor.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this._dialogParams!.supervisor.localize(
|
||||
"backup.could_not_create"
|
||||
),
|
||||
text: this._dialogParams!.supervisor.localize(
|
||||
"backup.create_blocked_not_running",
|
||||
{ state: this._dialogParams!.supervisor.info.state }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._creatingBackup = true;
|
||||
|
||||
this._error = "";
|
||||
if (backupDetails.password && !backupDetails.password.length) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"backup.enter_password"
|
||||
);
|
||||
this._creatingBackup = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
backupDetails.password &&
|
||||
backupDetails.password !== backupDetails.confirm_password
|
||||
) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"backup.passwords_not_matching"
|
||||
);
|
||||
this._creatingBackup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
delete backupDetails.confirm_password;
|
||||
|
||||
try {
|
||||
if (this._backupContent.backupType === "full") {
|
||||
await createHassioFullBackup(this.hass, backupDetails);
|
||||
} else {
|
||||
await createHassioPartialBackup(this.hass, backupDetails);
|
||||
}
|
||||
|
||||
this._dialogParams!.onCreate();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
this._creatingBackup = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-create-backup": HassioCreateBackupDialog;
|
||||
}
|
||||
}
|
||||
19
hassio/src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
19
hassio/src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "./dialog-hassio-backup-upload";
|
||||
|
||||
export interface HassioBackupUploadDialogParams {
|
||||
showBackup: (slug: string) => void;
|
||||
reloadBackup?: () => Promise<void>;
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const showBackupUploadDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupUploadDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup-upload",
|
||||
dialogImport: () => import("./dialog-hassio-backup-upload"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupLocationDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioBackupLocationDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupLocationDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup-location",
|
||||
dialogImport: () => import("./dialog-hassio-backup-location"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
21
hassio/src/dialogs/backup/show-dialog-hassio-backup.ts
Normal file
21
hassio/src/dialogs/backup/show-dialog-hassio-backup.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
slug: string;
|
||||
onDelete?: () => void;
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup",
|
||||
dialogImport: () => import("./dialog-hassio-backup"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioCreateBackupDialogParams {
|
||||
supervisor: Supervisor;
|
||||
onCreate: () => void;
|
||||
}
|
||||
|
||||
export const showHassioCreateBackupDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioCreateBackupDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-create-backup",
|
||||
dialogImport: () => import("./dialog-hassio-create-backup"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
184
hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts
Normal file
184
hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-select";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import type { DatadiskList } from "../../../../src/data/hassio/host";
|
||||
import { listDatadisks, moveDatadisk } from "../../../../src/data/hassio/host";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk";
|
||||
|
||||
const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => {
|
||||
// Assume a speed of 30 MB/s.
|
||||
const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30;
|
||||
const rebootTime = (supervisor.host.startup_time * 4) / 60;
|
||||
return Math.ceil((moveTime + rebootTime) / 10) * 10;
|
||||
});
|
||||
|
||||
@customElement("dialog-hassio-datadisk")
|
||||
class HassioDatadiskDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private dialogParams?: HassioDatatiskDialogParams;
|
||||
|
||||
@state() private selectedDevice?: string;
|
||||
|
||||
@state() private devices?: DatadiskList["devices"];
|
||||
|
||||
@state() private moving = false;
|
||||
|
||||
public showDialog(params: HassioDatatiskDialogParams) {
|
||||
this.dialogParams = params;
|
||||
listDatadisks(this.hass).then((data) => {
|
||||
this.devices = data.devices;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogParams = undefined;
|
||||
this.selectedDevice = undefined;
|
||||
this.devices = undefined;
|
||||
this.moving = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.moving
|
||||
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
|
||||
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
|
||||
@closed=${this.closeDialog}
|
||||
?hideActions=${this.moving}
|
||||
>
|
||||
${this.moving
|
||||
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.moving_desc"
|
||||
)}
|
||||
</p>`
|
||||
: html` ${this.devices?.length
|
||||
? html`
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.description",
|
||||
{
|
||||
current_path: this.dialogParams.supervisor.os.data_disk,
|
||||
time: calculateMoveTime(this.dialogParams.supervisor),
|
||||
}
|
||||
)}
|
||||
<br /><br />
|
||||
|
||||
<ha-select
|
||||
.label=${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.select_device"
|
||||
)}
|
||||
@selected=${this._selectDevice}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.devices.map(
|
||||
(device) =>
|
||||
html`<ha-list-item .value=${device}
|
||||
>${device}</ha-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: this.devices === undefined
|
||||
? this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.loading_devices"
|
||||
)
|
||||
: this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.no_devices"
|
||||
)}
|
||||
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.cancel"
|
||||
)}
|
||||
</ha-button>
|
||||
|
||||
<ha-button
|
||||
.disabled=${!this.selectedDevice}
|
||||
slot="primaryAction"
|
||||
@click=${this._moveDatadisk}
|
||||
>
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.move"
|
||||
)}
|
||||
</ha-button>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectDevice(ev) {
|
||||
this.selectedDevice = ev.target.value;
|
||||
}
|
||||
|
||||
private async _moveDatadisk() {
|
||||
this.moving = true;
|
||||
try {
|
||||
await moveDatadisk(this.hass, this.selectedDevice!);
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.dialogParams!.supervisor.localize(
|
||||
"system.host.failed_to_move"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-datadisk": HassioDatadiskDialog;
|
||||
}
|
||||
}
|
||||
17
hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts
Normal file
17
hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioDatatiskDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioDatadiskDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioDatatiskDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-datadisk",
|
||||
dialogImport: () => import("./dialog-hassio-datadisk"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
199
hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
Normal file
199
hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/search-input";
|
||||
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||
|
||||
const _filterDevices = memoizeOne(
|
||||
(hardware: HassioHardwareInfo, filter: string, language: string) =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
device.by_id?.toLowerCase().includes(filter) ||
|
||||
device.name.toLowerCase().includes(filter) ||
|
||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
@customElement("dialog-hassio-hardware")
|
||||
class HassioHardwareDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioHardwareDialogParams;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public showDialog(dialogParams: HassioHardwareDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const devices = _filterDevices(
|
||||
this._dialogParams.hardware,
|
||||
(this._filter || "").toLowerCase(),
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this._dialogParams.supervisor.localize(
|
||||
"dialog.hardware.title"
|
||||
)}
|
||||
>
|
||||
<div class="header" slot="heading">
|
||||
<h2>
|
||||
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
></ha-icon-button>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"dialog.hardware.search"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
|
||||
${devices.map(
|
||||
(device) =>
|
||||
html`<ha-expansion-panel
|
||||
.header=${device.name}
|
||||
.secondary=${device.by_id || undefined}
|
||||
outlined
|
||||
>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.subsystem"
|
||||
)}:
|
||||
</span>
|
||||
<span>${device.subsystem}</span>
|
||||
</div>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.device_path"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.dev_path}</code>
|
||||
</div>
|
||||
${device.by_id
|
||||
? html` <div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.id"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.by_id}</code>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="attributes">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.attributes"
|
||||
)}:
|
||||
</span>
|
||||
<pre>${dump(device.attributes, { indent: 2 })}</pre>
|
||||
</div>
|
||||
</ha-expansion-panel>`
|
||||
)}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
inset-inline-end: 16px;
|
||||
inset-inline-start: initial;
|
||||
top: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
h2 {
|
||||
margin: 18px 42px 0 18px;
|
||||
margin-inline-start: 18px;
|
||||
margin-inline-end: 42px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 4px 0;
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
code {
|
||||
font-size: var(--ha-font-size-s);
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
search-input {
|
||||
margin: 8px 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.device-property {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.attributes {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-hardware": HassioHardwareDialog;
|
||||
}
|
||||
}
|
||||
19
hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts
Normal file
19
hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioHardwareDialogParams {
|
||||
supervisor: Supervisor;
|
||||
hardware: HassioHardwareInfo;
|
||||
}
|
||||
|
||||
export const showHassioHardwareDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioHardwareDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-hardware",
|
||||
dialogImport: () => import("./dialog-hassio-hardware"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
70
hassio/src/dialogs/markdown/dialog-hassio-markdown.ts
Normal file
70
hassio/src/dialogs/markdown/dialog-hassio-markdown.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
|
||||
|
||||
@customElement("dialog-hassio-markdown")
|
||||
class HassioMarkdownDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public content!: string;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
public showDialog(params: HassioMarkdownDialogParams) {
|
||||
this.title = params.title;
|
||||
this.content = params.content;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this.title)}
|
||||
hideactions
|
||||
>
|
||||
<ha-markdown
|
||||
.content=${this.content || ""}
|
||||
dialogInitialFocus
|
||||
></ha-markdown>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
hassioStyle,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-markdown": HassioMarkdownDialog;
|
||||
}
|
||||
}
|
||||
17
hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts
Normal file
17
hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
|
||||
export interface HassioMarkdownDialogParams {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const showHassioMarkdownDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioMarkdownDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () => import("./dialog-hassio-markdown"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
647
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
647
hassio/src/dialogs/network/dialog-hassio-network.ts
Normal file
@@ -0,0 +1,647 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-password-field";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-tab-group";
|
||||
import "../../../../src/components/ha-tab-group-tab";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import type {
|
||||
AccessPoints,
|
||||
NetworkInterface,
|
||||
WifiConfiguration,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import {
|
||||
accesspointScan,
|
||||
updateNetworkInterface,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork
|
||||
extends LitElement
|
||||
implements HassDialog<HassioNetworkDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _accessPoints?: AccessPoints;
|
||||
|
||||
@state() private _curTabIndex = 0;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _interface?: NetworkInterface;
|
||||
|
||||
@state() private _interfaces!: NetworkInterface[];
|
||||
|
||||
@state() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _scanning = false;
|
||||
|
||||
@state() private _wifiConfiguration?: WifiConfiguration;
|
||||
|
||||
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._dirty = false;
|
||||
this._curTabIndex = 0;
|
||||
this.supervisor = params.supervisor;
|
||||
this._interfaces = params.supervisor.network.interfaces.sort((a, b) =>
|
||||
a.primary > b.primary ? -1 : 1
|
||||
);
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._processing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._interface) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.supervisor.localize("dialog.network.title")}
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this.supervisor.localize("dialog.network.title")}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
${this._interfaces.length > 1
|
||||
? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated}
|
||||
>${this._interfaces.map(
|
||||
(device, index) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.id=${device.interface}
|
||||
.panel=${index.toString()}
|
||||
.active=${this._curTabIndex === index}
|
||||
>
|
||||
${device.interface}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: ""}
|
||||
</div>
|
||||
${cache(this._renderTab())}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTab() {
|
||||
return html` <div class="form container">
|
||||
${IP_VERSIONS.map((version) =>
|
||||
this._interface![version] ? this._renderIPConfiguration(version) : ""
|
||||
)}
|
||||
${this._interface?.type === "wireless"
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
.header=${this.supervisor.localize("dialog.network.wifi")}
|
||||
outlined
|
||||
>
|
||||
${this._interface?.wifi?.ssid
|
||||
? html`<p>
|
||||
${this.supervisor.localize(
|
||||
"dialog.network.connected_to",
|
||||
{ ssid: this._interface?.wifi?.ssid }
|
||||
)}
|
||||
</p>`
|
||||
: ""}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
class="scan"
|
||||
@click=${this._scanForAP}
|
||||
.disabled=${this._scanning}
|
||||
.loading=${this._scanning}
|
||||
>
|
||||
${this.supervisor.localize("dialog.network.scan_ap")}
|
||||
</ha-button>
|
||||
${this._accessPoints &&
|
||||
this._accessPoints.accesspoints &&
|
||||
this._accessPoints.accesspoints.length !== 0
|
||||
? html`
|
||||
<ha-list>
|
||||
${this._accessPoints.accesspoints
|
||||
.filter((ap) => ap.ssid)
|
||||
.map(
|
||||
(ap) => html`
|
||||
<ha-list-item
|
||||
twoline
|
||||
@click=${this._selectAP}
|
||||
.activated=${ap.ssid ===
|
||||
this._wifiConfiguration?.ssid}
|
||||
.ap=${ap}
|
||||
>
|
||||
<span>${ap.ssid}</span>
|
||||
<span slot="secondary">
|
||||
${ap.mac} -
|
||||
${this.supervisor.localize(
|
||||
"dialog.network.signal_strength"
|
||||
)}:
|
||||
${ap.signal}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
`
|
||||
: ""}
|
||||
${this._wifiConfiguration
|
||||
? html`
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.open"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="open"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
undefined ||
|
||||
this._wifiConfiguration.auth === "open"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wep"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wep"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth === "wep"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wpa"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wpa-psk"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
"wpa-psk"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-password-field
|
||||
class="flex-auto"
|
||||
id="psk"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wifi_password"
|
||||
)}
|
||||
version="wifi"
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-password-field>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: ""}
|
||||
${this._dirty
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize("dialog.network.warning")}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this.closeDialog} appearance="plain">
|
||||
${this.supervisor.localize("common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateNetwork}
|
||||
.disabled=${!this._dirty}
|
||||
.loading=${this._processing}
|
||||
>
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _scanForAP() {
|
||||
if (!this._interface) {
|
||||
return;
|
||||
}
|
||||
this._scanning = true;
|
||||
try {
|
||||
this._accessPoints = await accesspointScan(
|
||||
this.hass,
|
||||
this._interface.interface
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to scan for accesspoints",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIPConfiguration(version: string) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
.header=${`IPv${version.charAt(version.length - 1)}`}
|
||||
outlined
|
||||
>
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.auto")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.static")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="static"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.disabled")}
|
||||
class="warning"
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="disabled"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "disabled"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._interface![version].method === "static"
|
||||
? html`
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="address"
|
||||
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].address)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
.label=${this.supervisor.localize("dialog.network.gateway")}
|
||||
.version=${version}
|
||||
.value=${this._interface![version].gateway}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
.label=${this.supervisor.localize("dialog.network.dns_servers")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].nameservers)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toArray(data: string | string[]): string[] {
|
||||
if (Array.isArray(data)) {
|
||||
if (data && typeof data[0] === "string") {
|
||||
data = data[0];
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
return data.replace(/ /g, "").split(",");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private _toString(data: string | string[]): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._processing = true;
|
||||
let interfaceOptions: Partial<NetworkInterface> = {};
|
||||
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
interfaceOptions[version] = {
|
||||
method: this._interface![version]?.method || "auto",
|
||||
};
|
||||
if (this._interface![version]?.method === "static") {
|
||||
interfaceOptions[version] = {
|
||||
...interfaceOptions[version],
|
||||
address: this._toArray(this._interface![version]?.address),
|
||||
gateway: this._interface![version]?.gateway,
|
||||
nameservers: this._toArray(this._interface![version]?.nameservers),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (this._wifiConfiguration) {
|
||||
interfaceOptions = {
|
||||
...interfaceOptions,
|
||||
wifi: {
|
||||
ssid: this._wifiConfiguration.ssid,
|
||||
mode: this._wifiConfiguration.mode,
|
||||
auth: this._wifiConfiguration.auth || "open",
|
||||
},
|
||||
};
|
||||
if (interfaceOptions.wifi!.auth !== "open") {
|
||||
interfaceOptions.wifi = {
|
||||
...interfaceOptions.wifi,
|
||||
psk: this._wifiConfiguration.psk,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interfaceOptions.enabled =
|
||||
this._wifiConfiguration !== undefined ||
|
||||
interfaceOptions.ipv4?.method !== "disabled" ||
|
||||
interfaceOptions.ipv6?.method !== "disabled";
|
||||
|
||||
try {
|
||||
await updateNetworkInterface(
|
||||
this.hass,
|
||||
this._interface!.interface,
|
||||
interfaceOptions
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.network.failed_to_change"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this._processing = false;
|
||||
return;
|
||||
}
|
||||
this._params?.loadData();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||
if (this._dirty) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("dialog.network.unsaved"),
|
||||
confirmText: this.supervisor.localize("common.yes"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = Number(ev.detail.name);
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as "disabled" | "auto" | "static";
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._interface[version]!.method === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
|
||||
this._interface[version]!.method = value;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as string as
|
||||
| "open"
|
||||
| "wep"
|
||||
| "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = source.id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._toString(this._interface[version]![id]) === this._toString(value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
this._interface[version]![id] = value;
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._wifiConfiguration ||
|
||||
this._wifiConfiguration![id] === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-position: static;
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
.container {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
|
||||
ha-button.scan {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 8px 4px;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 53px;
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), 16px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--primary-color: var(--error-color);
|
||||
}
|
||||
div.warning {
|
||||
margin: 12px 4px -12px;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
padding: 0 14px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 10px;
|
||||
}
|
||||
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-network": DialogHassioNetwork;
|
||||
}
|
||||
}
|
||||
19
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
19
hassio/src/dialogs/network/show-dialog-network.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import "./dialog-hassio-network";
|
||||
|
||||
export interface HassioNetworkDialogParams {
|
||||
supervisor: Supervisor;
|
||||
loadData: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showNetworkDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioNetworkDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-network",
|
||||
dialogImport: () => import("./dialog-hassio-network"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
255
hassio/src/dialogs/registries/dialog-hassio-registries.ts
Normal file
255
hassio/src/dialogs/registries/dialog-hassio-registries.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-button";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addHassioDockerRegistry,
|
||||
fetchHassioDockerRegistries,
|
||||
removeHassioDockerRegistry,
|
||||
} from "../../../../src/data/hassio/docker";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { RegistriesDialogParams } from "./show-dialog-registries";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "registry",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
required: true,
|
||||
selector: { text: { type: "password" } },
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("dialog-hassio-registries")
|
||||
class HassioRegistriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _registries?: {
|
||||
registry: string;
|
||||
username: string;
|
||||
}[];
|
||||
|
||||
@state() private _input: {
|
||||
registry?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
} = {};
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _addingRegistry = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._addingRegistry
|
||||
? this.supervisor.localize("dialog.registries.title_add")
|
||||
: this.supervisor.localize("dialog.registries.title_manage")
|
||||
)}
|
||||
>
|
||||
${this._addingRegistry
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${this._input}
|
||||
.schema=${SCHEMA}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabel}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<div class="action">
|
||||
<ha-button
|
||||
?disabled=${Boolean(
|
||||
!this._input.registry ||
|
||||
!this._input.username ||
|
||||
!this._input.password
|
||||
)}
|
||||
@click=${this._addNewRegistry}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.supervisor.localize("dialog.registries.add_registry")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: html`${this._registries?.length
|
||||
? this._registries.map(
|
||||
(entry) => html`
|
||||
<ha-settings-row class="registry">
|
||||
<span slot="heading"> ${entry.registry} </span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}:
|
||||
${entry.username}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.entry=${entry}
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._removeRegistry}
|
||||
></ha-icon-button>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.no_registries"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
<div class="action">
|
||||
<ha-button
|
||||
@click=${this._addRegistry}
|
||||
dialogInitialFocus
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.add_new_registry"
|
||||
)}
|
||||
</ha-button>
|
||||
</div> `}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.supervisor.localize(`dialog.registries.${schema.name}`);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._input = ev.detail.value;
|
||||
}
|
||||
|
||||
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
|
||||
this._opened = true;
|
||||
this._input = {};
|
||||
this.supervisor = dialogParams.supervisor;
|
||||
await this._loadRegistries();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._addingRegistry = false;
|
||||
this._opened = false;
|
||||
this._input = {};
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.updateComplete.then(() =>
|
||||
(
|
||||
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
|
||||
)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadRegistries(): Promise<void> {
|
||||
const registries = await fetchHassioDockerRegistries(this.hass);
|
||||
this._registries = Object.keys(registries!.registries).map((key) => ({
|
||||
registry: key,
|
||||
username: registries.registries[key].username,
|
||||
}));
|
||||
}
|
||||
|
||||
private _addRegistry(): void {
|
||||
this._addingRegistry = true;
|
||||
}
|
||||
|
||||
private async _addNewRegistry(): Promise<void> {
|
||||
const data = {};
|
||||
data[this._input.registry!] = {
|
||||
username: this._input.username,
|
||||
password: this._input.password,
|
||||
};
|
||||
|
||||
try {
|
||||
await addHassioDockerRegistry(this.hass, data);
|
||||
await this._loadRegistries();
|
||||
this._addingRegistry = false;
|
||||
this._input = {};
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_add"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeRegistry(ev: Event): Promise<void> {
|
||||
const entry = (ev.currentTarget as any).entry;
|
||||
|
||||
try {
|
||||
await removeHassioDockerRegistry(this.hass, entry.registry);
|
||||
await this._loadRegistries();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.registry {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.action {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--error-color);
|
||||
margin-right: -10px;
|
||||
margin-inline-end: -10px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-registries": HassioRegistriesDialog;
|
||||
}
|
||||
}
|
||||
18
hassio/src/dialogs/registries/show-dialog-registries.ts
Normal file
18
hassio/src/dialogs/registries/show-dialog-registries.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import "./dialog-hassio-registries";
|
||||
|
||||
export interface RegistriesDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showRegistriesDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: RegistriesDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-registries",
|
||||
dialogImport: () => import("./dialog-hassio-registries"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -3,80 +3,70 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../../components/ha-textfield";
|
||||
import "../../../../../components/ha-tooltip";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import type {
|
||||
HassioAddonInfo,
|
||||
HassioAddonsInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchStoreRepositories,
|
||||
removeStoreRepository,
|
||||
} from "../../../../../data/supervisor/store";
|
||||
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { RepositoryDialogParams } from "./show-dialog-repositories";
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
|
||||
@customElement("dialog-apps-repositories")
|
||||
class AppsRepositoriesDialog extends LitElement {
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@query("#repository_input", true) private _optionInput?: HaTextField;
|
||||
|
||||
@state() private _repositories?: HassioAddonRepository[];
|
||||
|
||||
@state() private _dialogParams?: RepositoryDialogParams;
|
||||
@state() private _dialogParams?: HassioRepositoryDialogParams;
|
||||
|
||||
@state() private _addon?: HassioAddonsInfo;
|
||||
|
||||
@state() private _open = false;
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public async showDialog(dialogParams: RepositoryDialogParams): Promise<void> {
|
||||
public async showDialog(
|
||||
dialogParams: HassioRepositoryDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._addon = dialogParams.addon;
|
||||
this._open = true;
|
||||
this._opened = true;
|
||||
await this._loadData();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._dialogParams?.closeCallback?.();
|
||||
this._dialogParams = undefined;
|
||||
this._addon = undefined;
|
||||
this._open = false;
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
fireEvent(this, "dialog-closed");
|
||||
}
|
||||
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core apps repository
|
||||
repo.slug !== "local" && // Locally managed apps
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "5c53de3b" && // The ESPHome repository
|
||||
repo.slug !== "d5369777" // Music Assistant repository
|
||||
)
|
||||
@@ -95,21 +85,23 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._addon || this._repositories === undefined) {
|
||||
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
const repositories = this._filteredRepositories(this._repositories);
|
||||
const usedRepositories = this._filteredUsedRepositories(
|
||||
repositories,
|
||||
this._addon.addons
|
||||
this._dialogParams.supervisor.addon.addons
|
||||
);
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.repositories.title"
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams!.supervisor.localize("dialog.repositories.title")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
@@ -131,10 +123,10 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
class="delete"
|
||||
slot="end"
|
||||
>
|
||||
${this.hass.localize(
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
usedRepositories.includes(repo.slug)
|
||||
? "ui.panel.config.apps.dialog.repositories.used"
|
||||
: "ui.panel.config.apps.dialog.repositories.remove"
|
||||
? "dialog.repositories.used"
|
||||
: "dialog.repositories.remove"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<div .id="icon-button-${repo.slug}">
|
||||
@@ -152,8 +144,8 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
`
|
||||
)
|
||||
: html`<ha-md-list-item
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.repositories.no_repositories"
|
||||
>${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.no_repositories"
|
||||
)}</ha-md-list-item
|
||||
>`}
|
||||
</ha-md-list>
|
||||
@@ -161,12 +153,12 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="repository_input"
|
||||
.value=${this._dialogParams?.url || ""}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.repositories.add"
|
||||
.value=${this._dialogParams!.url || ""}
|
||||
.label=${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
@keydown=${this._handleKeyAdd}
|
||||
autofocus
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-button
|
||||
.loading=${this._processing}
|
||||
@@ -175,17 +167,15 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.repositories.add"
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this._dialogParams?.supervisor.localize("common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -195,6 +185,9 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -204,8 +197,8 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
margin-top: 4px;
|
||||
}
|
||||
ha-button {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
div.delete ha-icon-button {
|
||||
@@ -219,6 +212,14 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() =>
|
||||
(
|
||||
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
|
||||
)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyAdd(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.key !== "Enter") {
|
||||
@@ -231,7 +232,7 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
try {
|
||||
this._repositories = await fetchStoreRepositories(this.hass);
|
||||
|
||||
fireEvent(this, "apps-collection-refresh", { collection: "addon" });
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
@@ -248,7 +249,7 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
await addStoreRepository(this.hass, input.value);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "apps-collection-refresh", { collection: "store" });
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
|
||||
input.value = "";
|
||||
} catch (err: any) {
|
||||
@@ -263,7 +264,7 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
await removeStoreRepository(this.hass, slug);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "apps-collection-refresh", { collection: "store" });
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
@@ -272,6 +273,6 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-apps-repositories": AppsRepositoriesDialog;
|
||||
"dialog-hassio-repositories": HassioRepositoriesDialog;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user