mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Merge branch 'dev' into blueprint-editor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
|
||||
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 `/developer-tools/state`
|
||||
You can find this information at `/config/developer-tools/state`
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -251,7 +251,6 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
@@ -621,7 +620,6 @@ 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)
|
||||
|
||||
@@ -713,7 +711,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (add-on not addon, integration not component)
|
||||
- [ ] Correct terminology (integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
|
||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -44,8 +44,3 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -94,27 +94,10 @@ jobs:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
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
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
# ℹ️ 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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
@@ -20,15 +20,15 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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, supervisor]
|
||||
bundle: [frontend]
|
||||
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@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
|
||||
- uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
43
.github/workflows/release.yaml
vendored
43
.github/workflows/release.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
with:
|
||||
abi: cp313
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
@@ -98,9 +98,9 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -118,32 +118,3 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
55
.vscode/tasks.json
vendored
55
.vscode/tasks.json
vendored
@@ -73,37 +73,6 @@
|
||||
"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",
|
||||
@@ -246,20 +215,6 @@
|
||||
"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",
|
||||
@@ -268,16 +223,6 @@
|
||||
}
|
||||
],
|
||||
"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,7 +14,6 @@ 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 = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
|
||||
[
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
// Icons in landingpage conflict with icons in HA so we don't load.
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||
),
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
@@ -36,7 +36,6 @@ 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__: `\`${
|
||||
@@ -289,26 +288,6 @@ 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,10 +24,6 @@ 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,29 +43,11 @@ 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(
|
||||
@@ -75,12 +57,3 @@ gulp.task(
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
);
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(
|
||||
compressHassioModernBrotli,
|
||||
compressHassioOtherBrotli,
|
||||
compressHassioModernZopfli,
|
||||
compressHassioOtherZopfli
|
||||
)
|
||||
);
|
||||
|
||||
@@ -266,28 +266,3 @@ 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,22 +123,11 @@ 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
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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,7 +9,6 @@ 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,7 +13,6 @@ import {
|
||||
createCastConfig,
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createHassioConfig,
|
||||
createLandingPageConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
@@ -159,31 +158,6 @@ 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,9 +170,7 @@ const setFragment = (fragment) => async () => {
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" &&
|
||||
fragment !== "supervisor" &&
|
||||
fragment !== "landing-page";
|
||||
fragment !== "base" && fragment !== "landing-page";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
@@ -207,18 +205,15 @@ const createTranslations = async () => {
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
// Remove the panels and landing-page to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
supervisor: undefined,
|
||||
"landing-page": 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"]), ""];
|
||||
@@ -318,11 +313,6 @@ 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,15 +49,5 @@ 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,7 +40,6 @@ const createRspackConfig = ({
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild,
|
||||
isLandingPageBuild,
|
||||
dontHash,
|
||||
}) => {
|
||||
@@ -168,13 +167,9 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||
bundle.emptyPackages({ isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle
|
||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||
.join("|")
|
||||
),
|
||||
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
@@ -205,6 +200,7 @@ 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",
|
||||
@@ -325,21 +321,6 @@ 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 }));
|
||||
|
||||
@@ -350,7 +331,6 @@ 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,11 +9,14 @@ 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";
|
||||
@@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let changeFunction;
|
||||
let sidebarChangeCallback;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
changeFunction?.({
|
||||
sidebarChangeCallback?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -16,15 +14,34 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS(
|
||||
"frontend/subscribe_system_data",
|
||||
(_msg, currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
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: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -7,8 +7,18 @@ 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: msg.template,
|
||||
result,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@@ -43,7 +43,6 @@ export default tseslint.config(
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
|
||||
@@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
1
gallery/src/pages/brand/logo.ts
Normal file
1
gallery/src/pages/brand/logo.ts
Normal file
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@@ -169,7 +169,7 @@ const SCHEMAS: {
|
||||
{
|
||||
title: "Selectors",
|
||||
translations: {
|
||||
addon: "Addon",
|
||||
app: "App",
|
||||
entity: "Entity",
|
||||
device: "Device",
|
||||
area: "Area",
|
||||
@@ -188,7 +188,7 @@ const SCHEMAS: {
|
||||
entities: "Entities",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
{ name: "app", selector: { app: {} } },
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "Attribute",
|
||||
|
||||
@@ -239,7 +239,7 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
app: { name: "App", selector: { app: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
|
||||
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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",
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Builds the Hass.io app for production
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-hassio
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Run the Hass.io development server
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-hassio
|
||||
@@ -1,248 +0,0 @@
|
||||
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,294 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
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,425 +0,0 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { relativeTime } from "../../../src/common/datetime/relative_time";
|
||||
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../src/components/data-table/ha-data-table";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-fab";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import {
|
||||
fetchHassioBackups,
|
||||
friendlyFolderName,
|
||||
reloadHassioBackups,
|
||||
removeBackup,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
|
||||
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
|
||||
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
type BackupItem = HassioBackup & {
|
||||
secondary: string;
|
||||
};
|
||||
|
||||
@customElement("hassio-backups")
|
||||
export class HassioBackups extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _selectedBackups: string[] = [];
|
||||
|
||||
@state() private _backups?: HassioBackup[] = [];
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||
if (backup.type === "full") {
|
||||
return this.supervisor.localize("backup.full_backup");
|
||||
}
|
||||
const content: string[] = [];
|
||||
if (backup.content.homeassistant) {
|
||||
content.push("Home Assistant");
|
||||
}
|
||||
if (backup.content.folders.length !== 0) {
|
||||
for (const folder of backup.content.folders) {
|
||||
content.push(friendlyFolderName[folder] || folder);
|
||||
}
|
||||
}
|
||||
|
||||
if (backup.content.addons.length !== 0) {
|
||||
for (const addon of backup.content.addons) {
|
||||
content.push(
|
||||
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
|
||||
?.name || addon
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return content.join(", ");
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass && this.isConnected) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
this._firstUpdatedCalled = true;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
|
||||
name: {
|
||||
title: this.supervisor.localize("backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
template: (backup) =>
|
||||
html`${backup.name || backup.slug}
|
||||
<div class="secondary">${backup.secondary}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: this.supervisor.localize("backup.size"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
backup.location || this.supervisor.localize("backup.data_disk"),
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
secondary: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
|
||||
backups.map((backup) => ({
|
||||
...backup,
|
||||
secondary: this._computeBackupContent(backup),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._isLoading) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this.supervisor.localize("backup.loading_backups")}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("backup.search")}
|
||||
.noDataText=${this.supervisor.localize("backup.no_backups")}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._backupData(this._backups || [])}
|
||||
id="slug"
|
||||
@row-click=${this._handleRowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
selectable
|
||||
has-fab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor?.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("common.reload")}
|
||||
</ha-list-item>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("dialog.backup_location.title")}
|
||||
</ha-list-item>
|
||||
${atLeastVersion(this.hass.config.version, 0, 116)
|
||||
? html`<ha-list-item>
|
||||
${this.supervisor.localize("backup.upload_backup")}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
|
||||
${this._selectedBackups.length
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.supervisor.localize("backup.selected", {
|
||||
number: this._selectedBackups.length,
|
||||
})}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
@click=${this._deleteSelected}
|
||||
>
|
||||
${this.supervisor.localize("backup.delete_selected")}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize(
|
||||
"backup.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</div>
|
||||
</div> `
|
||||
: ""}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
@click=${this._createBackup}
|
||||
.label=${this.supervisor.localize("backup.create_backup")}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._fetchBackups();
|
||||
break;
|
||||
case 1:
|
||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||
break;
|
||||
case 2:
|
||||
this._showUploadBackupDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedBackups = ev.detail.value;
|
||||
}
|
||||
|
||||
private _showUploadBackupDialog() {
|
||||
showBackupUploadDialog(this, {
|
||||
showBackup: (slug: string) =>
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
}),
|
||||
reloadBackup: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _fetchBackups() {
|
||||
this._isLoading = true;
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
this._isLoading = false;
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("backup.delete_backup_title"),
|
||||
text: this.supervisor.localize("backup.delete_backup_text", {
|
||||
number: this._selectedBackups.length,
|
||||
}),
|
||||
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("backup.failed_to_delete"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this._fetchBackups();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const slug = ev.detail.id;
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private _createBackup() {
|
||||
if (this.supervisor!.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor!.localize("backup.could_not_create"),
|
||||
text: this.supervisor!.localize("backup.create_blocked_not_running", {
|
||||
state: this.supervisor!.info.state,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioCreateBackupDialog(this, {
|
||||
supervisor: this.supervisor!,
|
||||
onCreate: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 58px;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: initial;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.header-toolbar .selected-txt {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-backups": HassioBackups;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,647 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,192 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
(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 %>");
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
@@ -1,146 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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,
|
||||
},
|
||||
];
|
||||
@@ -1,377 +0,0 @@
|
||||
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,231 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
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 { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HassioStats } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import type { SupervisorOptions } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
reloadSupervisor,
|
||||
restartSupervisor,
|
||||
setSupervisorOption,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta";
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/repairs/dialog-system-information";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo 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.supervisor.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("system.supervisor.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
return html`
|
||||
<ha-card header="Supervisor" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
supervisor-${this.supervisor.supervisor.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
supervisor-${this.supervisor.supervisor.version_latest}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.supervisor.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href="/hassio/update-available/supervisor"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.supervisor.channel")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.supervisor.channel}
|
||||
</span>
|
||||
${this.supervisor.supervisor.channel === "beta"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.leave_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.leave_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisor.supervisor.channel === "stable"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisor.supervisor.supported
|
||||
? !atLeastVersion(this.hass.config.version, 2021, 4)
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics"
|
||||
)}
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics_description"
|
||||
)}
|
||||
<button
|
||||
class="link"
|
||||
.title=${this.supervisor.localize("common.show_more")}
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisor.supervisor.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: ""
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_title"
|
||||
)}
|
||||
<ha-button
|
||||
slot="action"
|
||||
@click=${this._unsupportedDialog}
|
||||
variant="warning"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</ha-button>
|
||||
</ha-alert>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unhealthy_title"
|
||||
)}
|
||||
<ha-button
|
||||
variant="danger"
|
||||
size="small"
|
||||
slot="action"
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="metrics-block">
|
||||
${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
|
||||
@click=${this._supervisorReload}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.reload_supervisor"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize("system.supervisor.reload_supervisor")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._supervisorRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._metrics = await fetchHassioStats(this.hass, "supervisor");
|
||||
}
|
||||
|
||||
private async _toggleBeta(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
if (this.supervisor.supervisor.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => {
|
||||
await this._setChannel("beta");
|
||||
button.progress = false;
|
||||
},
|
||||
cancel: () => {
|
||||
button.progress = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this._setChannel("stable");
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setChannel(
|
||||
channel: SupervisorOptions["channel"]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await this._reloadSupervisor();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.failed_to_set_option"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await this._reloadSupervisor();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _reloadSupervisor(): Promise<void> {
|
||||
await reloadSupervisor(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "supervisor",
|
||||
});
|
||||
}
|
||||
|
||||
private async _supervisorRestart(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: "Supervisor",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restartSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.share_diagonstics_title"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"system.supervisor.share_diagonstics_description",
|
||||
{ line_break: html`<br /><br />` }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.unsupported_title"),
|
||||
text: html`${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_description"
|
||||
)} <br /><br />
|
||||
<ul>
|
||||
${this.supervisor.resolution.unsupported.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNSUPPORTED_REASON_URL[reason] ||
|
||||
`/more-info/unsupported/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
`system.supervisor.unsupported_reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _unhealthyDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
|
||||
text: html`${this.supervisor.localize(
|
||||
"system.supervisor.unhealthy_description"
|
||||
)} <br /><br />
|
||||
<ul>
|
||||
${this.supervisor.resolution.unhealthy.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNHEALTHY_REASON_URL[reason] ||
|
||||
`/more-info/unhealthy/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
`system.supervisor.unhealthy_reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisor.supervisor?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.failed_to_set_option"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.metrics-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-supervisor-info": HassioSupervisorInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-select";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "core",
|
||||
name: "Core",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("hassio-supervisor-log")
|
||||
class HassioSupervisorLog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _selectedLogProvider = "supervisor";
|
||||
|
||||
@state() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.supervisor.localize("system.log.log_provider")}
|
||||
@selected=${this._setLogProvider}
|
||||
.value=${this._selectedLogProvider}
|
||||
>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item .value=${provider.key}>
|
||||
${provider.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<ha-ansi-to-html .content=${this._content}>
|
||||
</ha-ansi-to-html>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._refresh}>
|
||||
${this.supervisor.localize("common.refresh")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.target.value;
|
||||
this._selectedLogProvider = provider;
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
await this._loadData();
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
const response = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
|
||||
this._content = await response.text();
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("system.log.get_logs", {
|
||||
provider: this._selectedLogProvider,
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-supervisor-log": HassioSupervisorLog;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
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 { hassioStyle } from "../resources/hassio-style";
|
||||
import "./hassio-core-info";
|
||||
import "./hassio-host-info";
|
||||
import "./hassio-supervisor-info";
|
||||
import "./hassio-supervisor-log";
|
||||
|
||||
@customElement("hassio-system")
|
||||
class HassioSystem 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 | undefined {
|
||||
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
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<hassio-core-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-core-info>
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<hassio-supervisor-log
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-supervisor-log>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
hassio-supervisor-log {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-system": HassioSystem;
|
||||
}
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
type CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
} 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 "../../../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-spinner";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-md-list";
|
||||
import "../../../src/components/ha-md-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
updateHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
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 { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"update-complete": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
os: "Home Assistant Operating System",
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
|
||||
type UpdateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
entry: UpdateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
if (entry === "addon") {
|
||||
return undefined;
|
||||
}
|
||||
if (entry === "core") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: version.includes("b")
|
||||
? "https://next.home-assistant.io/latest-release-notes/"
|
||||
: "https://www.home-assistant.io/latest-release-notes/";
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
|
||||
}
|
||||
if (entry === "supervisor") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/supervisor/commits/main"
|
||||
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@customElement("update-available-card")
|
||||
class UpdateAvailableCard 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: false }) public addonSlug?: string;
|
||||
|
||||
@state() private _updateType?: UpdateType;
|
||||
|
||||
@state() private _changelogContent?: string;
|
||||
|
||||
@state() private _addonInfo?: HassioAddonDetails;
|
||||
|
||||
@state() private _updating = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._updateType ||
|
||||
(this._updateType === "addon" && !this._addonInfo)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const changelog = changelogUrl(this._updateType, this._version_latest);
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("update_available.update_name", {
|
||||
name: this._name,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._version === this._version_latest
|
||||
? html`<p>
|
||||
${this.supervisor.localize("update_available.no_update", {
|
||||
name: this._name,
|
||||
})}
|
||||
</p>`
|
||||
: !this._updating
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: nothing}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"update_available.description",
|
||||
{
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
${createBackupTexts
|
||||
? html`
|
||||
<hr />
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${createBackupTexts.title}
|
||||
</span>
|
||||
|
||||
${createBackupTexts.description
|
||||
? html`
|
||||
<span slot="supporting-text">
|
||||
${createBackupTexts.description}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id="create-backup"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: html`<ha-spinner
|
||||
aria-label="Updating"
|
||||
size="large"
|
||||
></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
})}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._version !== this._version_latest && !this._updating
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
? html`
|
||||
<ha-button
|
||||
href=${changelog}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<span></span>
|
||||
<ha-progress-button @click=${this._update}>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const pathPart = this.route?.path.substring(1, this.route.path.length);
|
||||
const updateType = ["core", "os", "supervisor"].includes(pathPart)
|
||||
? pathPart
|
||||
: "addon";
|
||||
this._updateType = updateType as UpdateType;
|
||||
|
||||
switch (updateType) {
|
||||
case "addon":
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
break;
|
||||
case "core":
|
||||
this._loadCoreData();
|
||||
break;
|
||||
case "supervisor":
|
||||
this._loadSupervisorData();
|
||||
break;
|
||||
case "os":
|
||||
this._loadOsData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
// Addon backup
|
||||
if (
|
||||
this._updateType === "addon" &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 2, 0)
|
||||
) {
|
||||
const version = this._version;
|
||||
return {
|
||||
title: this.supervisor.localize("update_available.create_backup.addon"),
|
||||
description: this.supervisor.localize(
|
||||
"update_available.create_backup.addon_description",
|
||||
{ version: version }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Old behavior
|
||||
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
|
||||
return {
|
||||
title: this.supervisor.localize(
|
||||
"update_available.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version
|
||||
: this.supervisor[this._updateType]?.version || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _version_latest(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version_latest
|
||||
: this.supervisor[this._updateType]?.version_latest || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _name(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.name
|
||||
: SUPERVISOR_UPDATE_NAMES[this._updateType]
|
||||
: "";
|
||||
}
|
||||
|
||||
private async _loadAddonData() {
|
||||
try {
|
||||
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const addonStoreInfo =
|
||||
!this._addonInfo.detached && !this._addonInfo.available
|
||||
? this._addonStoreInfo(
|
||||
this._addonInfo.slug,
|
||||
this.supervisor.store.addons
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this._addonInfo.changelog) {
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
this.addonSlug!
|
||||
);
|
||||
this._changelogContent = extractChangelog(this._addonInfo, content);
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._addonInfo.available && addonStoreInfo) {
|
||||
if (
|
||||
!addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this._addonInfo.arch
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSupervisorData() {
|
||||
try {
|
||||
const supervisor = await fetchHassioSupervisorInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { supervisor });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCoreData() {
|
||||
try {
|
||||
const core = await fetchHassioHomeAssistantInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { core });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadOsData() {
|
||||
try {
|
||||
const os = await fetchHassioHassOsInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { os });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
|
||||
this._error = this.supervisor.localize("backup.backup_already_running");
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._updating = true;
|
||||
|
||||
try {
|
||||
if (this._updateType === "addon") {
|
||||
await updateHassioAddon(
|
||||
this.hass,
|
||||
this.addonSlug!,
|
||||
this._shouldCreateBackup
|
||||
);
|
||||
} else if (this._updateType === "core") {
|
||||
await updateCore(this.hass, this._shouldCreateBackup);
|
||||
} else if (this._updateType === "os") {
|
||||
await updateOS(this.hass);
|
||||
} else if (this._updateType === "supervisor") {
|
||||
await updateSupervisor(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._updating = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "update-complete");
|
||||
this._updating = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-card": UpdateAvailableCard;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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 { goBack } from "../../../src/common/navigate";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import "./update-available-card";
|
||||
|
||||
@customElement("update-available-dashboard")
|
||||
class UpdateAvailableDashboard 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 {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
>
|
||||
<update-available-card
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
@update-complete=${this._updateComplete}
|
||||
></update-available-card>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
goBack();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--primary-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
update-available-card {
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-dashboard": UpdateAvailableDashboard;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
81
package.json
81
package.json
@@ -27,32 +27,32 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.3",
|
||||
"@codemirror/view": "6.39.9",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.1.2",
|
||||
"@formatjs/intl-displaynames": "7.1.2",
|
||||
"@formatjs/intl-durationformat": "0.9.2",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.2",
|
||||
"@formatjs/intl-listformat": "8.1.2",
|
||||
"@formatjs/intl-locale": "5.1.2",
|
||||
"@formatjs/intl-numberformat": "9.1.2",
|
||||
"@formatjs/intl-pluralrules": "6.1.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.2",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
"@formatjs/intl-listformat": "8.2.1",
|
||||
"@formatjs/intl-locale": "5.2.1",
|
||||
"@formatjs/intl-numberformat": "9.2.2",
|
||||
"@formatjs/intl-pluralrules": "6.2.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.2",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -71,7 +71,6 @@
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-menu": "0.27.0",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-snackbar": "0.27.0",
|
||||
@@ -89,7 +88,7 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -97,7 +96,7 @@
|
||||
"barcode-detector": "3.0.8",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.47.0",
|
||||
"core-js": "3.48.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -112,7 +111,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.0.9",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -122,7 +121,7 @@
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
@@ -133,7 +132,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -146,18 +145,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.8",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.4.0",
|
||||
"@rspack/core": "1.7.2",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -176,7 +175,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -192,14 +191,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.1",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.4.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -207,17 +206,17 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.7.4",
|
||||
"rspack-manifest-plugin": "5.2.0",
|
||||
"prettier": "3.8.1",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.2",
|
||||
"tar": "7.5.7",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.53.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -229,13 +228,13 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.0.0",
|
||||
"globals": "17.3.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251229.0"
|
||||
version = "20260128.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||
]
|
||||
requires-python = ">=3.13.0"
|
||||
requires-python = ">=3.14.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
@@ -16,6 +16,12 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||
nvm uninstall "$nodeCurrent"
|
||||
fi
|
||||
|
||||
# install yarn if not already available
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
npm install -g corepack
|
||||
yes | yarn
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
|
||||
16
script/core
16
script/core
@@ -53,23 +53,9 @@ logger:
|
||||
default: info
|
||||
logs:
|
||||
homeassistant.components.frontend: debug
|
||||
" >> "${WD}/config/configuration.yaml"
|
||||
|
||||
if [ -n "${HASSIO}" ]; then
|
||||
echo "
|
||||
# frontend:
|
||||
# development_repo: ${WD}
|
||||
|
||||
hassio:
|
||||
development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
else
|
||||
echo "
|
||||
frontend:
|
||||
development_repo: ${WD}
|
||||
|
||||
# hassio:
|
||||
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
fi
|
||||
development_repo: ${WD}" >> "${WD}/config/configuration.yaml" >> "${WD}/config/configuration.yaml"
|
||||
|
||||
if [ -n "${CODESPACES}" ]; then
|
||||
echo "
|
||||
|
||||
@@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
@@ -24,14 +24,48 @@ export const THEME_COLORS = new Set([
|
||||
"blue-grey",
|
||||
"black",
|
||||
"white",
|
||||
]);
|
||||
|
||||
const YAML_ONLY_THEMES_COLORS = new Set([
|
||||
"primary-text",
|
||||
"secondary-text",
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
export function computeCssColor(color: string): string {
|
||||
if (THEME_COLORS.has(color)) {
|
||||
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
|
||||
return `var(--${color}-color)`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid color.
|
||||
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
|
||||
*/
|
||||
export function isValidColorString(color: string | undefined): boolean {
|
||||
if (!color || typeof color !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a theme color
|
||||
if (THEME_COLORS.has(color)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a hex color
|
||||
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(color)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a valid CSS color name by trying to parse it
|
||||
// Use CSS.supports() for a more efficient test without DOM manipulation
|
||||
// This checks if the browser recognizes the color value
|
||||
try {
|
||||
const style = new Option().style;
|
||||
style.color = color;
|
||||
return style.color !== "";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user