mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-05 07:48:17 +00:00
Compare commits
2 Commits
protocol-b
...
padding-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22d29ef4f5 | ||
|
|
f65d41fea6 |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-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
|
||||
|
||||
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -621,6 +621,7 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
@@ -712,7 +713,7 @@ 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
39
.github/workflows/ci.yaml
vendored
39
.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@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
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
|
||||
@@ -94,10 +94,27 @@ jobs:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
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:
|
||||
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
|
||||
# ℹ️ 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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
|
||||
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
|
||||
|
||||
8
.github/workflows/nightly.yaml
vendored
8
.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
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
bundle: [frontend, supervisor]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
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
|
||||
|
||||
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",
|
||||
|
||||
@@ -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,14 @@ 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 &&
|
||||
// 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 +36,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 +289,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,9 +168,13 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
bundle.emptyPackages({ isLandingPageBuild }).length
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
|
||||
new RegExp(
|
||||
bundle
|
||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||
.join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
@@ -200,7 +205,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 +213,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",
|
||||
@@ -321,6 +323,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 +348,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);
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -27,25 +27,4 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
// 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) => {
|
||||
onChange?.({
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,6 +43,7 @@ export default tseslint.config(
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
|
||||
@@ -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";
|
||||
@@ -239,7 +239,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: {
|
||||
|
||||
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,28 +1,31 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../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 { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
} 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;
|
||||
@@ -41,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
|
||||
@@ -51,8 +52,8 @@ 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}
|
||||
@@ -71,8 +72,8 @@ class SupervisorAppAudio extends LitElement {
|
||||
</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}
|
||||
@@ -95,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>
|
||||
@@ -105,7 +106,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
supervisorAppsStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host,
|
||||
ha-card {
|
||||
@@ -118,7 +119,7 @@ class SupervisorAppAudio extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select:last-child {
|
||||
margin-top: var(--ha-space-2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -152,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 {
|
||||
@@ -171,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];
|
||||
}
|
||||
@@ -197,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;
|
||||
@@ -211,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,18 +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;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: var(--ha-space-4);
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -79,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) }
|
||||
);
|
||||
}
|
||||
@@ -89,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;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ 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
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -2,22 +2,24 @@ 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 "../../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
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 "../../../../../data/hassio/docker";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
} 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 = [
|
||||
{
|
||||
@@ -37,10 +39,12 @@ const SCHEMA = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("dialog-apps-registries")
|
||||
class AppsRegistriesDialog extends LitElement {
|
||||
@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;
|
||||
@@ -67,12 +71,8 @@ class AppsRegistriesDialog extends LitElement {
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._addingRegistry
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.title_add"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.title_manage"
|
||||
)
|
||||
? this.supervisor.localize("dialog.registries.title_add")
|
||||
: this.supervisor.localize("dialog.registries.title_manage")
|
||||
)}
|
||||
>
|
||||
${this._addingRegistry
|
||||
@@ -96,9 +96,7 @@ class AppsRegistriesDialog extends LitElement {
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.add_registry"
|
||||
)}
|
||||
${this.supervisor.localize("dialog.registries.add_registry")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
@@ -108,15 +106,15 @@ class AppsRegistriesDialog extends LitElement {
|
||||
<ha-settings-row class="registry">
|
||||
<span slot="heading"> ${entry.registry} </span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.username"
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}:
|
||||
${entry.username}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.entry=${entry}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.remove"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._removeRegistry}
|
||||
@@ -126,8 +124,8 @@ class AppsRegistriesDialog extends LitElement {
|
||||
)
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.no_registries"
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.no_registries"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
@@ -139,8 +137,8 @@ class AppsRegistriesDialog extends LitElement {
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.add_new_registry"
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.add_new_registry"
|
||||
)}
|
||||
</ha-button>
|
||||
</div> `}
|
||||
@@ -149,17 +147,16 @@ class AppsRegistriesDialog extends LitElement {
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.apps.dialog.registries.${schema.name}` as any
|
||||
);
|
||||
this.supervisor.localize(`dialog.registries.${schema.name}`);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._input = ev.detail.value;
|
||||
}
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
|
||||
this._opened = true;
|
||||
this._input = {};
|
||||
this.supervisor = dialogParams.supervisor;
|
||||
await this._loadRegistries();
|
||||
await this.updateComplete;
|
||||
}
|
||||
@@ -204,9 +201,7 @@ class AppsRegistriesDialog extends LitElement {
|
||||
this._input = {};
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.failed_to_add"
|
||||
),
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_add"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -220,9 +215,7 @@ class AppsRegistriesDialog extends LitElement {
|
||||
await this._loadRegistries();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dialog.registries.failed_to_remove"
|
||||
),
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -257,6 +250,6 @@ class AppsRegistriesDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-apps-registries": AppsRegistriesDialog;
|
||||
"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,44 +3,41 @@ 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 { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
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 { 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 _addon?: HassioAddonsInfo;
|
||||
@state() private _dialogParams?: HassioRepositoryDialogParams;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@@ -48,16 +45,16 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
|
||||
@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._opened = true;
|
||||
await this._loadData();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialogParams?.closeCallback?.();
|
||||
this._dialogParams = undefined;
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
@@ -67,9 +64,9 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
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
|
||||
)
|
||||
@@ -88,13 +85,13 @@ 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
|
||||
@@ -104,7 +101,7 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.apps.dialog.repositories.title")
|
||||
this._dialogParams!.supervisor.localize("dialog.repositories.title")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
@@ -126,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}">
|
||||
@@ -147,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>
|
||||
@@ -156,9 +153,9 @@ 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}
|
||||
dialogInitialFocus
|
||||
@@ -170,14 +167,14 @@ 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-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
${this._dialogParams?.supervisor.localize("common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -200,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 {
|
||||
@@ -235,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);
|
||||
}
|
||||
@@ -252,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) {
|
||||
@@ -267,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);
|
||||
}
|
||||
@@ -276,6 +273,6 @@ class AppsRepositoriesDialog extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-apps-repositories": AppsRepositoriesDialog;
|
||||
"dialog-hassio-repositories": HassioRepositoriesDialog;
|
||||
}
|
||||
}
|
||||
19
hassio/src/dialogs/repositories/show-dialog-repositories.ts
Normal file
19
hassio/src/dialogs/repositories/show-dialog-repositories.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-repositories";
|
||||
|
||||
export interface HassioRepositoryDialogParams {
|
||||
supervisor: Supervisor;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const showRepositoriesDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioRepositoryDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-repositories",
|
||||
dialogImport: () => import("./dialog-hassio-repositories"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
38
hassio/src/dialogs/suggestAddonRestart.ts
Normal file
38
hassio/src/dialogs/suggestAddonRestart.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import { restartHassioAddon } from "../../../src/data/hassio/addon";
|
||||
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 type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export const suggestAddonRestart = async (
|
||||
element: LitElement,
|
||||
hass: HomeAssistant,
|
||||
supervisor: Supervisor,
|
||||
addon: HassioAddonDetails
|
||||
): Promise<void> => {
|
||||
const confirmed = await showConfirmationDialog(element, {
|
||||
title: supervisor.localize("dialog.restart_addon.title", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: supervisor.localize("dialog.restart_addon.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_addon.restart"),
|
||||
dismissText: supervisor.localize("common.cancel"),
|
||||
});
|
||||
if (confirmed) {
|
||||
try {
|
||||
await restartHassioAddon(hass, addon.slug);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(element, {
|
||||
title: supervisor.localize("common.failed_to_restart_name", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
192
hassio/src/dialogs/system-managed/dialog-system-managed.ts
Normal file
192
hassio/src/dialogs/system-managed/dialog-system-managed.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { mdiClose, mdiPuzzle, mdiSwapHorizontal } 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 "../../../../src/components/ha-dialog-header";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-icon-next";
|
||||
import "../../../../src/components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
getConfigEntry,
|
||||
type ConfigEntry,
|
||||
} from "../../../../src/data/config_entries";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
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 { brandsUrl } from "../../../../src/util/brands-url";
|
||||
import type { SystemManagedDialogParams } from "./show-dialog-system-managed";
|
||||
|
||||
@customElement("dialog-system-managed")
|
||||
class HassioSystemManagedDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _supervisor?: Supervisor;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _configEntry?: ConfigEntry;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: SystemManagedDialogParams
|
||||
): Promise<void> {
|
||||
this._addon = dialogParams.addon;
|
||||
this._supervisor = dialogParams.supervisor;
|
||||
this._open = true;
|
||||
this._loadConfigEntry();
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._addon = undefined;
|
||||
this._supervisor = undefined;
|
||||
this._configEntry = undefined;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._addon || !this._open || !this._supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const addonImage =
|
||||
atLeastVersion(this.hass.config.version, 0, 105) && this._addon.icon
|
||||
? `/api/hassio/addons/${this._addon.slug}/icon`
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._addon?.name}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="icons">
|
||||
<ha-svg-icon
|
||||
class="primary"
|
||||
.path=${mdiHomeAssistant}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
${addonImage
|
||||
? html`<img src=${addonImage} alt=${this._addon.name} />`
|
||||
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
|
||||
</div>
|
||||
${this._supervisor.localize("addon.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("addon.system_managed.description")}
|
||||
${this._configEntry
|
||||
? html`
|
||||
<h3>
|
||||
${this._supervisor.localize(
|
||||
"addon.system_managed.managed_by"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href=${`/config/integrations/integration/${this._configEntry.domain}`}
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
class="integration-icon"
|
||||
alt=${this._configEntry.title}
|
||||
src=${brandsUrl({
|
||||
domain: this._configEntry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
${this._configEntry.title}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private async _loadConfigEntry() {
|
||||
if (this._addon?.system_managed_config_entry) {
|
||||
try {
|
||||
const { config_entry } = await getConfigEntry(
|
||||
this.hass,
|
||||
this._addon.system_managed_config_entry
|
||||
);
|
||||
this._configEntry = config_entry;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
--mdc-icon-size: 48px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.icons img {
|
||||
width: 48px;
|
||||
}
|
||||
.icons .primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.integration-icon {
|
||||
width: 24px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 4px;
|
||||
--md-list-item-trailing-space: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-system-managed": HassioSystemManagedDialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface SystemManagedDialogParams {
|
||||
addon: HassioAddonDetails;
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showSystemManagedDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SystemManagedDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-system-managed",
|
||||
dialogImport: () => import("./dialog-system-managed"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
23
hassio/src/entrypoint.js.template
Normal file
23
hassio/src/entrypoint.js.template
Normal file
@@ -0,0 +1,23 @@
|
||||
(function () {
|
||||
function loadES5(src) {
|
||||
var el = document.createElement("script");
|
||||
el.src = src;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (<%= modernRegex %>.test(navigator.userAgent)) {
|
||||
try {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
new Function("import('<%= entry %>')")();
|
||||
<% } %>
|
||||
} catch (err) {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
|
||||
28
hassio/src/entrypoint.ts
Normal file
28
hassio/src/entrypoint.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
haFontFamilyBody,
|
||||
haFontSmoothing,
|
||||
haMozOsxFontSmoothing,
|
||||
} from "../../src/resources/theme/typography.globals";
|
||||
import "./hassio-main";
|
||||
|
||||
import("../../src/resources/append-ha-style");
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = `
|
||||
body {
|
||||
font-family: ${haFontFamilyBody};
|
||||
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
|
||||
-webkit-font-smoothing: ${haFontSmoothing};
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
146
hassio/src/hassio-main.ts
Normal file
146
hassio/src/hassio-main.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import "./hassio-router";
|
||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||
|
||||
@customElement("hassio-main")
|
||||
export class HassioMain extends SupervisorBaseElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._applyTheme();
|
||||
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
// supervisor UI can also run under older versions of Home Assistant.
|
||||
// So here we are going to translate toggle events into the appropriate
|
||||
// open and close events. These events are a no-op in newer versions of
|
||||
// Home Assistant.
|
||||
this.addEventListener("hass-toggle-menu", () => {
|
||||
fireEvent(
|
||||
(window.parent as any).customPanel,
|
||||
// @ts-ignore
|
||||
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
|
||||
);
|
||||
});
|
||||
// Paulus - March 19, 2019
|
||||
// We changed the navigate event to fire directly on the window, as that's
|
||||
// where we are listening for it. However, the older panel_custom will
|
||||
// listen on this element for navigation events, so we need to forward them.
|
||||
|
||||
// Joakim - April 26, 2021
|
||||
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
|
||||
mainWindow.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Paulus - May 17, 2021
|
||||
// Convert the <a> tags to native nav in Home Assistant < 2021.6
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward haptic events to parent window.
|
||||
window.addEventListener("haptic", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(window.parent, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Forward keydown events to the main window for quickbar access
|
||||
document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
|
||||
// Ignore if modifier keys are pressed
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
if (oldHass.themes !== this.hass.themes) {
|
||||
this._applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hassio-router
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-router>
|
||||
`;
|
||||
}
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
themeSettings = this.hass.selectedTheme;
|
||||
} else {
|
||||
themeName =
|
||||
(this.hass.selectedTheme as unknown as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
themeSettings,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-main": HassioMain;
|
||||
}
|
||||
}
|
||||
155
hassio/src/hassio-my-redirect.ts
Normal file
155
hassio/src/hassio-my-redirect.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParamsObject,
|
||||
} from "../../src/common/url/search-params";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import "../../src/layouts/hass-error-screen";
|
||||
import type {
|
||||
ParamType,
|
||||
Redirect,
|
||||
Redirects,
|
||||
} from "../../src/panels/my/ha-panel-my";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
|
||||
export const REDIRECTS: Redirects = {
|
||||
supervisor: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_logs: {
|
||||
redirect: "/hassio/system",
|
||||
},
|
||||
supervisor_info: {
|
||||
redirect: "/hassio/system",
|
||||
},
|
||||
supervisor_snapshots: {
|
||||
redirect: "/hassio/backups",
|
||||
},
|
||||
supervisor_backups: {
|
||||
redirect: "/hassio/backups",
|
||||
},
|
||||
supervisor_store: {
|
||||
redirect: "/hassio/store",
|
||||
},
|
||||
supervisor_addons: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_addon: {
|
||||
redirect: "/hassio/addon",
|
||||
params: {
|
||||
addon: "string",
|
||||
},
|
||||
optional_params: {
|
||||
repository_url: "url",
|
||||
},
|
||||
},
|
||||
supervisor_ingress: {
|
||||
redirect: "/hassio/ingress",
|
||||
params: {
|
||||
addon: "string",
|
||||
},
|
||||
},
|
||||
supervisor_add_addon_repository: {
|
||||
redirect: "/hassio/store",
|
||||
params: {
|
||||
repository_url: "url",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hassio-my-redirect")
|
||||
class HassioMyRedirect extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() public _error?: TemplateResult | string;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const path = this.route.path.substr(1);
|
||||
const redirect = REDIRECTS[path];
|
||||
|
||||
if (!redirect) {
|
||||
this._error = this.supervisor.localize("my.not_supported", {
|
||||
link: html`<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://my.home-assistant.io/faq.html#supported-pages"
|
||||
>
|
||||
${this.supervisor.localize("my.faq_link")}
|
||||
</a>`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
try {
|
||||
url = this._createRedirectUrl(redirect);
|
||||
} catch (_err: any) {
|
||||
this._error = this.supervisor.localize("my.error");
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(url, { replace: true });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<hass-error-screen
|
||||
.error=${this._error}
|
||||
></hass-error-screen>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _createRedirectUrl(redirect: Redirect): string {
|
||||
const params = this._createRedirectParams(redirect);
|
||||
return `${redirect.redirect}${params}`;
|
||||
}
|
||||
|
||||
private _createRedirectParams(redirect: Redirect): string {
|
||||
const params = extractSearchParamsObject();
|
||||
if (!redirect.params && !Object.keys(params).length) {
|
||||
return "";
|
||||
}
|
||||
const resultParams = {};
|
||||
Object.entries(redirect.params || {}).forEach(([key, type]) => {
|
||||
if (!params[key] || !this._checkParamType(type, params[key])) {
|
||||
throw Error();
|
||||
}
|
||||
resultParams[key] = params[key];
|
||||
});
|
||||
Object.entries(redirect.optional_params || {}).forEach(([key, type]) => {
|
||||
if (params[key]) {
|
||||
if (!this._checkParamType(type, params[key])) {
|
||||
throw Error();
|
||||
}
|
||||
resultParams[key] = params[key];
|
||||
}
|
||||
});
|
||||
return `?${createSearchParam(resultParams)}`;
|
||||
}
|
||||
|
||||
private _checkParamType(type: ParamType, value: string) {
|
||||
if (type === "string") {
|
||||
return true;
|
||||
}
|
||||
if (type === "url") {
|
||||
return value && value === sanitizeUrl(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-my-redirect": HassioMyRedirect;
|
||||
}
|
||||
}
|
||||
53
hassio/src/hassio-panel-router.ts
Normal file
53
hassio/src/hassio-panel-router.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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, Route } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./dashboard/hassio-dashboard";
|
||||
|
||||
@customElement("hassio-panel-router")
|
||||
class HassioPanelRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
beforeRender: (page: string) =>
|
||||
page === "snapshots" ? "backups" : undefined,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-dashboard",
|
||||
},
|
||||
store: {
|
||||
tag: "hassio-addon-store",
|
||||
load: () => import("./addon-store/hassio-addon-store"),
|
||||
},
|
||||
backups: {
|
||||
tag: "hassio-backups",
|
||||
load: () => import("./backups/hassio-backups"),
|
||||
},
|
||||
system: {
|
||||
tag: "hassio-system",
|
||||
load: () => import("./system/hassio-system"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
el.route = this.route;
|
||||
el.narrow = this.narrow;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-panel-router": HassioPanelRouter;
|
||||
}
|
||||
}
|
||||
55
hassio/src/hassio-panel.ts
Normal file
55
hassio/src/hassio-panel.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-panel-router";
|
||||
|
||||
@customElement("hassio-panel")
|
||||
class HassioPanel 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;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(supervisorCollection).some(
|
||||
(collection) => !this.supervisor[collection]
|
||||
)
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-panel": HassioPanel;
|
||||
}
|
||||
}
|
||||
91
hassio/src/hassio-router.ts
Normal file
91
hassio/src/hassio-router.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
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";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
|
||||
@customElement("hassio-router")
|
||||
class HassioRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
beforeRender: (page: string) => {
|
||||
if (page === "snapshots") {
|
||||
return "backups";
|
||||
}
|
||||
if (page === "dashboard" && this.panel.config?.ingress) {
|
||||
return "ingress";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
backups: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
"update-available": {
|
||||
tag: "update-available-dashboard",
|
||||
load: () => import("./update-available/update-available-dashboard"),
|
||||
},
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () => import("./addon-view/hassio-addon-dashboard"),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () => import("./ingress-view/hassio-ingress-view"),
|
||||
},
|
||||
_my_redirect: {
|
||||
tag: "hassio-my-redirect",
|
||||
load: () => import("./hassio-my-redirect"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const hassioPanel = el.localName === "hassio-panel";
|
||||
const ingressPanel = el.localName === "hassio-ingress-view";
|
||||
const route = hassioPanel
|
||||
? this.route
|
||||
: ingressPanel && this.panel.config?.ingress
|
||||
? this._ingressRoute(this.panel.config?.ingress)
|
||||
: this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.route = route;
|
||||
el.supervisor = this.supervisor;
|
||||
|
||||
if (ingressPanel) {
|
||||
el.ingressPanel = Boolean(this.panel.config?.ingress);
|
||||
}
|
||||
}
|
||||
|
||||
private _ingressRoute = memoizeOne((ingress: string) => ({
|
||||
prefix: "/hassio/ingress",
|
||||
path: `/${ingress}`,
|
||||
}));
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-router": HassioRouter;
|
||||
}
|
||||
}
|
||||
34
hassio/src/hassio-tabs.ts
Normal file
34
hassio/src/hassio-tabs.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
mdiBackupRestore,
|
||||
mdiCogs,
|
||||
mdiPuzzle,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
atLeastVersion(hass.config.version, 2022, 5)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
377
hassio/src/ingress-view/hassio-ingress-view.ts
Normal file
377
hassio/src/ingress-view/hassio-ingress-view.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import { nextRender } from "../../../src/common/util/render-status";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
startHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
createHassioSession,
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
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-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
|
||||
@customElement("hassio-ingress-view")
|
||||
class HassioIngressView extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public ingressPanel = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _loadingMessage?: string;
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this._loadingMessage}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const iframe = html`<iframe
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
>
|
||||
</iframe>`;
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.header=${this._addon.name}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${iframe}
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
|
||||
? html`<div class="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@click=${this._toggleMenu}
|
||||
></ha-icon-button>
|
||||
<div class="main-title">${this._addon.name}</div>
|
||||
</div>
|
||||
${iframe}`
|
||||
: iframe}`;
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
if (this.route.path === "") {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
let addonInfo: HassioAddonDetails;
|
||||
if (requestedAddon) {
|
||||
try {
|
||||
addonInfo = await fetchHassioAddonInfo(this.hass, requestedAddon);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
title: requestedAddon,
|
||||
});
|
||||
await nextRender();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
return;
|
||||
}
|
||||
if (!addonInfo.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_not_installed"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||
} else if (!addonInfo.ingress) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||
} else {
|
||||
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("route")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addon = this.route.path.substring(1);
|
||||
|
||||
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
|
||||
|
||||
if (addon && addon !== oldAddon) {
|
||||
this._loadingMessage = undefined;
|
||||
this._fetchData(addon);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass);
|
||||
|
||||
let addon: HassioAddonDetails;
|
||||
|
||||
try {
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (_err: any) {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_info") ||
|
||||
"Unable to fetch add-on info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.version) {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_installed") ||
|
||||
"The add-on is not installed. Please install it first",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_supported") ||
|
||||
"This add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.state || !["startup", "started"].includes(addon.state)) {
|
||||
await this.updateComplete;
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_running") ||
|
||||
"The add-on is not running. Do you want to start it now?",
|
||||
title: addon.name,
|
||||
confirmText:
|
||||
this.supervisor.localize("ingress.start_addon") || "Start add-on",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
await startHassioAddon(this.hass, addonSlug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
this._fetchData(addonSlug);
|
||||
return;
|
||||
} catch (_err) {
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_starting_addon") ||
|
||||
"Error starting the add-on",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (addon.state === "startup") {
|
||||
// Addon is starting up, wait for it to start
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
|
||||
this._fetchDataTimeout = window.setTimeout(() => {
|
||||
this._fetchData(addonSlug);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._loadingMessage = undefined;
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
|
||||
let session: string;
|
||||
|
||||
try {
|
||||
session = await createSessionPromise;
|
||||
} catch (_err: any) {
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_creating_session") ||
|
||||
"Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
this._sessionKeepAlive = window.setInterval(async () => {
|
||||
try {
|
||||
await validateHassioSession(this.hass, session);
|
||||
} catch (_err: any) {
|
||||
session = await createHassioSession(this.hass);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
private async _checkLoaded(ev): Promise<void> {
|
||||
if (!this._addon) {
|
||||
return;
|
||||
}
|
||||
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
|
||||
await this.updateComplete;
|
||||
showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_ready") ||
|
||||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
confirm: async () => {
|
||||
const addon = this._addon;
|
||||
this._addon = undefined;
|
||||
await Promise.all([
|
||||
this.updateComplete,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
}),
|
||||
]);
|
||||
this._addon = addon;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.header + iframe {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-l);
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ingress-view": HassioIngressView;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const supervisorAppsStyle = css`
|
||||
export const hassioStyle = css`
|
||||
.content {
|
||||
margin: var(--ha-space-2);
|
||||
margin: 8px;
|
||||
}
|
||||
h1,
|
||||
.description,
|
||||
@@ -11,21 +11,21 @@ export const supervisorAppsStyle = css`
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
margin-bottom: 8px;
|
||||
font-family: var(--ha-font-family-body);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.description {
|
||||
margin-top: var(--ha-space-1);
|
||||
padding-left: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.card-group {
|
||||
@@ -50,6 +50,6 @@ export const supervisorAppsStyle = css`
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
231
hassio/src/supervisor-base-element.ts
Normal file
231
hassio/src/supervisor-base-element.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import type { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { computeLocalize } from "../../src/common/translations/localize";
|
||||
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
|
||||
import type { HassioResponse } from "../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import { fetchNetworkInfo } from "../../src/data/hassio/network";
|
||||
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
|
||||
import type {
|
||||
Supervisor,
|
||||
SupervisorObject,
|
||||
SupervisorKeys,
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
getSupervisorEventCollection,
|
||||
supervisorCollection,
|
||||
cleanupSupervisorCollection,
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
import {
|
||||
computeRTLDirection,
|
||||
setDirectionStyles,
|
||||
} from "../../src/common/util/compute_rtl";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"supervisor-update": Partial<Supervisor>;
|
||||
"supervisor-collection-refresh": { collection: SupervisorObject };
|
||||
}
|
||||
}
|
||||
|
||||
export class SupervisorBaseElement extends urlSyncMixin(
|
||||
ProvideHassLitMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
|
||||
localize: () => "",
|
||||
};
|
||||
|
||||
@state() private _unsubs: Record<string, UnsubscribeFunc> = {};
|
||||
|
||||
@state() private _collections: Record<string, Collection<unknown>> = {};
|
||||
|
||||
@state() private _language = "en";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
if (this.route?.prefix === "/hassio") {
|
||||
this._initSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
Object.keys(this._unsubs).forEach((unsub) => {
|
||||
this._unsubs[unsub]();
|
||||
delete this._unsubs[unsub];
|
||||
});
|
||||
Object.keys(this._collections).forEach((collection) => {
|
||||
cleanupSupervisorCollection(this.hass.connection, collection);
|
||||
});
|
||||
this._collections = {};
|
||||
this.removeEventListener(
|
||||
"supervisor-collection-refresh",
|
||||
this._handleSupervisorStoreRefreshEvent
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (this.route?.prefix === "/hassio") {
|
||||
this._initSupervisor();
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
const oldHass = changedProperties.get("hass") as
|
||||
| HomeAssistant
|
||||
| undefined;
|
||||
if (oldHass?.language !== this.hass.language) {
|
||||
this._language = this.hass.language;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("_language") || !this.hasUpdated) {
|
||||
this._initializeLocalize();
|
||||
this._applyDirection(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateSupervisor(update: Partial<Supervisor>): void {
|
||||
this.supervisor = { ...this.supervisor, ...update };
|
||||
}
|
||||
|
||||
private async _initializeLocalize() {
|
||||
const { language, data } = await getTranslation(null, this._language);
|
||||
|
||||
this._updateSupervisor({
|
||||
localize: await computeLocalize<SupervisorKeys>(
|
||||
this.constructor.prototype,
|
||||
language,
|
||||
{
|
||||
[language]: data,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleSupervisorStoreRefreshEvent(ev) {
|
||||
const collection = ev.detail.collection;
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
if (collection in this._collections) {
|
||||
this._collections[collection].refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.hass.callApi<HassioResponse<any>>(
|
||||
"GET",
|
||||
`hassio${supervisorCollection[collection]}`
|
||||
);
|
||||
this._updateSupervisor({ [collection]: response.data });
|
||||
}
|
||||
|
||||
private _subscribeCollection(collection: string) {
|
||||
if (this._unsubs[collection]) {
|
||||
this._unsubs[collection]();
|
||||
}
|
||||
try {
|
||||
this._unsubs[collection] = this._collections[collection].subscribe(
|
||||
(data) =>
|
||||
this._updateSupervisor({
|
||||
[collection]: data,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initSupervisor(): Promise<void> {
|
||||
this.addEventListener(
|
||||
"supervisor-collection-refresh",
|
||||
this._handleSupervisorStoreRefreshEvent
|
||||
);
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
Object.keys(supervisorCollection).forEach((collection) => {
|
||||
if (collection in this._collections) {
|
||||
this._subscribeCollection(collection);
|
||||
this._collections[collection].refresh();
|
||||
} else {
|
||||
this._collections[collection] = getSupervisorEventCollection(
|
||||
this.hass.connection,
|
||||
collection,
|
||||
supervisorCollection[collection]
|
||||
);
|
||||
if (this._collections[collection].state) {
|
||||
// happens when the grace period of the collection unsubscribe has not passed yet
|
||||
this._updateSupervisor({
|
||||
[collection]: this._collections[collection].state,
|
||||
});
|
||||
}
|
||||
this._subscribeCollection(collection);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [
|
||||
addon,
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
store,
|
||||
] = await Promise.all([
|
||||
fetchHassioAddonsInfo(this.hass),
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
fetchHassioHassOsInfo(this.hass),
|
||||
fetchNetworkInfo(this.hass),
|
||||
fetchHassioResolution(this.hass),
|
||||
fetchSupervisorStore(this.hass),
|
||||
]);
|
||||
|
||||
this._updateSupervisor({
|
||||
addon,
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
store,
|
||||
});
|
||||
|
||||
this.addEventListener("supervisor-update", (ev) =>
|
||||
this._updateSupervisor(ev.detail)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyDirection(hass: HomeAssistant) {
|
||||
const direction = computeRTLDirection(hass);
|
||||
setDirectionStyles(direction, this);
|
||||
}
|
||||
}
|
||||
206
hassio/src/system/hassio-core-info.ts
Normal file
206
hassio/src/system/hassio-core-info.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import type { HassioStats } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { restartCore } from "../../../src/data/supervisor/core";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-core-info")
|
||||
class HassioCoreInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _metrics?: HassioStats;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("system.core.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("system.core.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-card header="Core" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
core-${this.supervisor.core.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
core-${this.supervisor.core.version_latest}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.core.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
href="/hassio/update-available/core"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div>
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
slot="primaryAction"
|
||||
variant="danger"
|
||||
@click=${this._coreRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Core",
|
||||
})}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", { name: "Core" })}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._metrics = await fetchHassioStats(this.hass, "core");
|
||||
}
|
||||
|
||||
private async _coreRestart(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("confirm.restart.title", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restartCore(this.hass);
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 124px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-core-info": HassioCoreInfo;
|
||||
}
|
||||
}
|
||||
453
hassio/src/system/hassio-host-info.ts
Normal file
453
hassio/src/system/hassio-host-info.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
changeHostOptions,
|
||||
configSyncOS,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import type { NetworkInfo } from "../../../src/data/hassio/network";
|
||||
import { fetchNetworkInfo } from "../../../src/data/hassio/network";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../src/util/calculate";
|
||||
import "../components/supervisor-metric";
|
||||
import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk";
|
||||
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
|
||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
class HassioHostInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
const primaryIpAddress = this.supervisor.host.features.includes("network")
|
||||
? this._primaryIpAddress(this.supervisor.network!)
|
||||
: "";
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("system.host.used_space"),
|
||||
value: this._getUsedSpace(
|
||||
this.supervisor.host.disk_used,
|
||||
this.supervisor.host.disk_total
|
||||
),
|
||||
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
|
||||
},
|
||||
];
|
||||
return html`
|
||||
<ha-card header="Host" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${this.supervisor.host.features.includes("hostname")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.hostname")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.hostname}
|
||||
</span>
|
||||
<ha-button
|
||||
@click=${this._changeHostnameClicked}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("system.host.change")}
|
||||
</ha-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.supervisor.host.features.includes("network")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.ip_address")}
|
||||
</span>
|
||||
<span slot="description"> ${primaryIpAddress} </span>
|
||||
<ha-button
|
||||
@click=${this._changeNetworkClicked}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("system.host.change")}
|
||||
</ha-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.operating_system")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.operating_system}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.os.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href="/hassio/update-available/os"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
${!this.supervisor.host.features.includes("haos")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.docker_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.info.docker}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.supervisor.host.deployment
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.deployment")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.deployment}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
${this.supervisor.host.disk_life_time !== null
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.lifetime_used")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.disk_life_time} %
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.supervisor.host.features.includes("reboot")
|
||||
? html`
|
||||
<ha-progress-button variant="danger" @click=${this._hostReboot}>
|
||||
${this.supervisor.localize("system.host.reboot_host")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisor.host.features.includes("shutdown")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
${this.supervisor.localize("system.host.shutdown_host")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
.action=${"hardware"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.hardware")}
|
||||
</ha-list-item>
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? html`
|
||||
<ha-list-item
|
||||
.action=${"import_from_usb"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.import_from_usb")}
|
||||
</ha-list-item>
|
||||
${this.supervisor.host.features.includes("os_agent") &&
|
||||
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
|
||||
? html`
|
||||
<ha-list-item
|
||||
.action=${"move_datadisk"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.host.move_datadisk"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _getUsedSpace = memoizeOne((used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total))
|
||||
);
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info || !network_info.interfaces) {
|
||||
return "";
|
||||
}
|
||||
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
|
||||
});
|
||||
|
||||
private async _handleMenuAction(ev) {
|
||||
switch ((ev.target as any).action) {
|
||||
case "hardware":
|
||||
await this._showHardware();
|
||||
break;
|
||||
case "import_from_usb":
|
||||
await this._importFromUSB();
|
||||
break;
|
||||
case "move_datadisk":
|
||||
await this._moveDatadisk();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDatadisk(): void {
|
||||
showHassioDatadiskDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
});
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
let hardware;
|
||||
try {
|
||||
hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.host.failed_to_get_hardware_list"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
|
||||
}
|
||||
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("system.host.reboot_host"),
|
||||
text: this.supervisor.localize("system.host.confirm_reboot"),
|
||||
confirmText: this.supervisor.localize("system.host.reboot_host"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_reboot"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("system.host.shutdown_host"),
|
||||
text: this.supervisor.localize("system.host.confirm_shutdown"),
|
||||
confirmText: this.supervisor.localize("system.host.shutdown_host"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_shutdown"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.supervisor.host.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: this.supervisor.localize("system.host.change_hostname"),
|
||||
inputLabel: this.supervisor.localize("system.host.new_hostname"),
|
||||
inputType: "string",
|
||||
defaultValue: curHostname,
|
||||
confirmText: this.supervisor.localize("common.update"),
|
||||
});
|
||||
|
||||
if (hostname && hostname !== curHostname) {
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "host",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "host",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.host.failed_to_import_from_usb"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "network",
|
||||
});
|
||||
} else {
|
||||
const network = await fetchNetworkInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { network });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 124px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
ha-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-host-info": HassioHostInfo;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user