Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein
22d29ef4f5 Use regular item for bottom padding in combobox 2026-01-05 11:24:57 +01:00
Paul Bottein
f65d41fea6 Add warning about running tsc with file arguments
When tsc receives file arguments, it ignores tsconfig.json and emits
.js files into src/, polluting the codebase. This documents the issue
and provides cleanup instructions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 10:51:00 +01:00
642 changed files with 22216 additions and 24813 deletions

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.14
FROM mcr.microsoft.com/devcontainers/python:1-3.13
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
<!--
If your issue is about how an entity is shown in the UI, please add the state
and attributes for all situations with a screenshot of the UI.
You can find this information at `/config/developer-tools/state`
You can find this information at `/developer-tools/state`
-->
```yaml

View File

@@ -621,6 +621,7 @@ this.hass.localize("ui.panel.config.updates.update_available", {
#### Key Terminology
- **"add-on"** (hyphenated, not "addon")
- **"integration"** (preferred over "component")
- **Technical terms**: Use lowercase (automation, entity, device, service)
@@ -712,7 +713,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
- [ ] American English spelling
- [ ] Friendly, informational tone
- [ ] Avoids abbreviations and jargon
- [ ] Correct terminology (integration not component)
- [ ] Correct terminology (add-on not addon, integration not component)
### Component-Specific Checks

5
.github/labeler.yml vendored
View File

@@ -44,3 +44,8 @@ GitHub Actions:
- any-glob-to-any-file:
- .github/workflows/**
- .github/*.yml
Supervisor:
- changed-files:
- any-glob-to-any-file:
- hassio/src/**

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -56,12 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: |
node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -94,10 +94,27 @@ jobs:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
supervisor:
name: Build supervisor
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: frontend-build
path: hass_frontend/
name: supervisor-bundle-stats
path: build/stats/*.json
if-no-files-found: error
retention-days: 7

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
permissions:
@@ -20,15 +20,15 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -12,7 +12,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
bundle: [frontend]
bundle: [frontend, supervisor]
build: [modern, legacy]
runs-on: ubuntu-latest
steps:

View File

@@ -18,6 +18,6 @@ jobs:
pull-requests: read
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
- uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -19,17 +19,14 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -37,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -49,18 +46,14 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install build
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
@@ -84,7 +77,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
with:
abi: cp314
abi: cp313
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}
@@ -98,9 +91,9 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -118,3 +111,32 @@ jobs:
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
release-supervisor:
name: Release supervisor frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build supervisor
run: hassio/script/build_hassio
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Upload Translations
run: |

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/
/node_modules/
yarn-error.log
npm-debug.log

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.12.0

55
.vscode/tasks.json vendored
View File

@@ -73,6 +73,37 @@
"instanceLimit": 1
}
},
{
"label": "Develop Supervisor panel",
"type": "gulp",
"task": "develop-hassio",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Gallery",
"type": "gulp",
@@ -215,6 +246,20 @@
"instanceLimit": 1
}
},
{
"label": "Run HA Core for Supervisor in devcontainer",
"type": "shell",
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [],
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Setup and fetch nightly translations",
"type": "gulp",
@@ -223,6 +268,16 @@
}
],
"inputs": [
{
"id": "supervisorHost",
"type": "promptString",
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
},
{
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",

View File

@@ -14,6 +14,7 @@ This is the repository for the official [Home Assistant](https://home-assistant.
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
- Production build: `script/build_frontend`
- Gallery: `cd gallery && script/develop_gallery`
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
## Frontend development

View File

@@ -18,14 +18,14 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
[
// Icons in landingpage conflict with icons in HA so we don't load.
isLandingPageBuild &&
// Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
isLandingPageBuild &&
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -36,6 +36,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
@@ -288,6 +289,26 @@ module.exports.config = {
};
},
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
return {
name: "supervisor" + nameSuffix(latestBuild),
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
},
outputPath: outputPath(paths.hassio_output_root, latestBuild),
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
isHassioBuild: true,
defineOverlay: {
__SUPERVISOR__: true,
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
},
};
},
gallery({ isProdBuild, latestBuild }) {
return {
name: "gallery" + nameSuffix(latestBuild),

View File

@@ -24,6 +24,10 @@ gulp.task(
)
);
gulp.task("clean-hassio", async () =>
deleteSync([paths.hassio_output_root, paths.build_dir])
);
gulp.task(
"clean-gallery",
gulp.parallel("clean-translations", async () =>

View File

@@ -43,11 +43,29 @@ const compressAppModernBrotli = () =>
const compressAppModernZopfli = () =>
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioModernBrotli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"brotli"
);
const compressHassioModernZopfli = () =>
compressModern(
paths.hassio_output_root,
paths.hassio_output_latest,
"zopfli"
);
const compressAppOtherBrotli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
const compressAppOtherZopfli = () =>
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
const compressHassioOtherBrotli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
const compressHassioOtherZopfli = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
gulp.task(
"compress-app",
gulp.parallel(
@@ -57,3 +75,12 @@ gulp.task(
compressAppOtherZopfli
)
);
gulp.task(
"compress-hassio",
gulp.parallel(
compressHassioModernBrotli,
compressHassioOtherBrotli,
compressHassioModernZopfli,
compressHassioOtherZopfli
)
);

View File

@@ -266,3 +266,28 @@ gulp.task(
paths.landingPage_output_es5
)
);
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task(
"gen-pages-hassio-dev",
genPagesDevTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
"src",
paths.hassio_publicPath
)
);
gulp.task(
"gen-pages-hassio-prod",
genPagesProdTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
paths.hassio_output_latest,
paths.hassio_output_es5,
"src"
)
);

View File

@@ -123,11 +123,22 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-translations-landing-page", async () => {
const staticDir = paths.landingPage_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
copyFonts(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@@ -0,0 +1,45 @@
import gulp from "gulp";
import env from "../env.cjs";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-hassio",
"gen-dummy-icons-json",
"gen-pages-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-watch-hassio"
)
);
gulp.task(
"build-hassio",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-hassio",
"gen-dummy-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])
)
);

View File

@@ -9,6 +9,7 @@ import "./fetch-nightly-translations.js";
import "./gallery.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./hassio.js";
import "./landing-page.js";
import "./locale-data.js";
import "./rspack.js";

View File

@@ -13,6 +13,7 @@ import {
createCastConfig,
createDemoConfig,
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
} from "../rspack.cjs";
@@ -158,6 +159,31 @@ gulp.task("rspack-prod-cast", () =>
)
);
gulp.task("rspack-watch-hassio", () => {
// This command will run forever because we don't close compiler
rspack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
})
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("rspack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);
gulp.task("rspack-dev-server-gallery", () =>
runDevServer({
compiler: rspack(

View File

@@ -170,7 +170,9 @@ const setFragment = (fragment) => async () => {
};
const panelFragment = (fragment) =>
fragment !== "base" && fragment !== "landing-page";
fragment !== "base" &&
fragment !== "supervisor" &&
fragment !== "landing-page";
const HASHES = new Map();
@@ -205,15 +207,18 @@ const createTranslations = async () => {
FRAGMENTS.map((fragment) => {
switch (fragment) {
case "base":
// Remove the panels and landing-page to create the base translations
// Remove the panels and supervisor to create the base translations
return [
flatten({
...data,
ui: { ...data.ui, panel: undefined },
"landing-page": undefined,
supervisor: undefined,
}),
"",
];
case "supervisor":
// Supervisor key is at the top level
return [flatten(data.supervisor), ""];
case "landing-page":
// landing-page key is at the top level
return [flatten(data["landing-page"]), ""];
@@ -313,6 +318,11 @@ gulp.task(
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(setFragment("supervisor"), "build-translations")
);
gulp.task(
"build-landing-page-translations",
gulp.series(setFragment("landing-page"), "build-translations")

View File

@@ -49,5 +49,15 @@ module.exports = {
"../landing-page/dist/static"
),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"
),
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
hassio_publicPath: "/api/hassio/app",
translations_src: path.resolve(__dirname, "../src/translations"),
};

View File

@@ -40,6 +40,7 @@ const createRspackConfig = ({
latestBuild,
isStatsBuild,
isTestBuild,
isHassioBuild,
isLandingPageBuild,
dontHash,
}) => {
@@ -167,9 +168,13 @@ const createRspackConfig = ({
);
},
}),
bundle.emptyPackages({ isLandingPageBuild }).length
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
? new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
new RegExp(
bundle
.emptyPackages({ isHassioBuild, isLandingPageBuild })
.join("|")
),
path.resolve(paths.root_dir, "src/util/empty.js")
)
: false,
@@ -200,7 +205,6 @@ const createRspackConfig = ({
"lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js",
"lit/directives/ref$": "lit/directives/ref.js",
"lit/directives/class-map$": "lit/directives/class-map.js",
"lit/directives/style-map$": "lit/directives/style-map.js",
"lit/directives/if-defined$": "lit/directives/if-defined.js",
@@ -209,9 +213,7 @@ const createRspackConfig = ({
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": latestBuild
? "lit/directives/keyed.js"
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
"lit/directives/keyed$": "lit/directives/keyed.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
@@ -321,6 +323,21 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
const createCastConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
}) =>
createRspackConfig(
bundle.config.hassio({
isProdBuild,
latestBuild,
isStatsBuild,
isTestBuild,
})
);
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
@@ -331,6 +348,7 @@ module.exports = {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,

View File

@@ -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);
}

View File

@@ -9,14 +9,11 @@ import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockLabelRegistry } from "./stubs/label_registry";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@@ -63,9 +60,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (

View File

@@ -27,25 +27,4 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS(
"frontend/subscribe_system_data",
(_msg, currentHass, onChange) => {
onChange?.({
value: currentHass.systemData,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}
);
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
onChange?.({
preview_feature: _msg.preview_feature,
domain: _msg.domain,
enabled: false,
is_built_in: true,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
};

View File

@@ -7,18 +7,8 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
})
);
hass.mockWS("render_template", (msg, _hass, onChange) => {
let result = msg.template;
// Simple variable substitution for demo purposes
if (msg.variables) {
for (const [key, value] of Object.entries(msg.variables)) {
result = result.replace(
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
String(value)
);
}
}
onChange!({
result,
result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false },
});
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@@ -43,6 +43,7 @@ export default tseslint.config(
__BUILD__: false,
__VERSION__: false,
__STATIC_PATH__: false,
__SUPERVISOR__: false,
},
parser: tseslint.parser,

View File

@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -169,7 +169,7 @@ const SCHEMAS: {
{
title: "Selectors",
translations: {
app: "App",
addon: "Addon",
entity: "Entity",
device: "Device",
area: "Area",
@@ -188,7 +188,7 @@ const SCHEMAS: {
entities: "Entities",
},
schema: [
{ name: "app", selector: { app: {} } },
{ name: "addon", selector: { addon: {} } },
{ name: "entity", selector: { entity: {} } },
{
name: "Attribute",

View File

@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
@@ -239,7 +239,7 @@ const SCHEMAS: {
selector: { config_entry: {} },
},
duration: { name: "Duration", selector: { duration: {} } },
app: { name: "App", selector: { app: {} } },
addon: { name: "Addon", selector: { addon: {} } },
number_box: {
name: "Number Box",
selector: {

9
hassio/config.cjs Normal file
View File

@@ -0,0 +1,9 @@
const path = require("path");
module.exports = {
// Target directory for the build.
buildDir: path.resolve(__dirname, "build"),
nodeDir: path.resolve(__dirname, "../node_modules"),
// Path where the Hass.io frontend will be publicly available.
publicPath: "/api/hassio/app",
};

9
hassio/script/build_hassio Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Builds the Hass.io app for production
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-hassio

9
hassio/script/develop Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Run the Hass.io development server
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp develop-hassio

View File

@@ -3,20 +3,24 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-card";
import type { HassioAddonRepository } from "../../../data/hassio/addon";
import type { StoreAddon } from "../../../data/supervisor/store";
import type { HomeAssistant } from "../../../types";
import "./components/supervisor-apps-card-content";
import { filterAndSort } from "./components/supervisor-apps-filter";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import type { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
import { hassioStyle } from "../resources/hassio-style";
@customElement("supervisor-apps-repository")
export class SupervisorAppsRepositoryEl extends LitElement {
@customElement("hassio-addon-repository")
export class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: StoreAddon[];
@@ -46,12 +50,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
return html`
<div class="content">
<p class="description">
${this.hass.localize(
"ui.panel.config.apps.store.no_results_found",
{
repository: repo.name,
}
)}
${this.supervisor.localize("store.no_results_found", {
repository: repo.name,
})}
</p>
</div>
`;
@@ -69,7 +70,7 @@ export class SupervisorAppsRepositoryEl extends LitElement {
@click=${this._addonTapped}
>
<div class="card-content">
<supervisor-apps-card-content
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
@@ -79,19 +80,13 @@ export class SupervisorAppsRepositoryEl extends LitElement {
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? this.hass.localize(
"ui.panel.config.apps.state.update_available"
)
: this.hass.localize(
"ui.panel.config.apps.state.installed"
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.state.installed")
: addon.available
? this.hass.localize(
"ui.panel.config.apps.state.not_installed"
)
: this.hass.localize(
"ui.panel.config.apps.state.not_available"
)}
? this.supervisor.localize("addon.state.not_installed")
: this.supervisor.localize("addon.state.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"
@@ -99,7 +94,11 @@ export class SupervisorAppsRepositoryEl extends LitElement {
: !addon.available
? "not_available"
: ""}
.iconImage=${addon.icon
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
.showTopbar=${addon.installed || !addon.available}
@@ -110,7 +109,7 @@ export class SupervisorAppsRepositoryEl extends LitElement {
: !addon.available
? "unavailable"
: ""}
></supervisor-apps-card-content>
></hassio-card-content>
</div>
</ha-card>
`
@@ -121,12 +120,12 @@ export class SupervisorAppsRepositoryEl extends LitElement {
}
private _addonTapped(ev) {
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`);
}
static get styles(): CSSResultGroup {
return [
supervisorAppsStyle,
hassioStyle,
css`
ha-card {
cursor: pointer;
@@ -145,6 +144,6 @@ export class SupervisorAppsRepositoryEl extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-repository": SupervisorAppsRepositoryEl;
"hassio-addon-repository": HassioAddonRepositoryEl;
}
}

View File

@@ -0,0 +1,248 @@
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { mdiDotsVertical } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/search-input";
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { StoreAddon } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") {
return -1;
}
if (b.slug === "local") {
return 1;
}
if (a.slug === "core") {
return -1;
}
if (b.slug === "core") {
return 1;
}
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
};
@customElement("hassio-addon-store")
export class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _filter?: string;
public async refreshData() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
this._loadData();
}
}
protected render() {
let repos: (TemplateResult | typeof nothing)[] = [];
if (this.supervisor.store.repositories) {
repos = this.addonRepositories(
this.supervisor.store.repositories,
this.supervisor.store.addons,
this._filter
);
}
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this.supervisor.localize("panel.store")}
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item>
${this.supervisor.localize("store.check_updates")}
</ha-list-item>
<ha-list-item>
${this.supervisor.localize("store.repositories")}
</ha-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<ha-list-item>
${this.supervisor.localize("store.registries")}
</ha-list-item>`
: ""}
</ha-button-menu>
${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
: html`
<div class="search">
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._filterChanged}
></search-input>
</div>
${repos}
`}
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
<a href="/profile" target="_top">
${this.supervisor.localize("store.missing_addons")}
</a>
</div>
`
: ""}
</hass-subpage>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate("/hassio/store", { replace: true });
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this._loadData();
}
private addonRepositories = memoizeOne(
(
repositories: HassioAddonRepository[],
addons: StoreAddon[],
filter?: string
) =>
repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
);
return filteredAddons.length !== 0
? html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${filter!}
.supervisor=${this.supervisor}
></hassio-addon-repository>
`
: nothing;
})
);
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.refreshData();
break;
case 1:
this._manageRepositoriesClicked();
break;
case 2:
this._manageRegistries();
break;
}
}
private _apiCalled(ev) {
if (ev.detail.success) {
this._loadData();
}
}
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
url,
});
}
private _manageRegistries() {
showRegistriesDialog(this, { supervisor: this.supervisor });
}
private _loadData() {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
private _filterChanged(e) {
this._filter = e.detail.value;
}
static styles = css`
hassio-addon-repository {
margin-top: 24px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.advanced {
padding: 12px;
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.advanced a {
margin-left: 0.5em;
margin-inline-start: 0.5em;
margin-inline-end: initial;
color: var(--primary-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-store": HassioAddonStore;
}
}

View File

@@ -1,28 +1,31 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-select";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../../data/hassio/addon";
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
import type { HassioHardwareAudioDevice } from "../../../../../data/hassio/hardware";
import { fetchHassioHardwareAudio } from "../../../../../data/hassio/hardware";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
} from "../../../../src/data/hassio/addon";
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
import type { HassioHardwareAudioDevice } from "../../../../src/data/hassio/hardware";
import { fetchHassioHardwareAudio } from "../../../../src/data/hassio/hardware";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("supervisor-app-audio")
class SupervisorAppAudio extends LitElement {
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ type: Boolean }) public disabled = false;
@@ -41,9 +44,7 @@ class SupervisorAppAudio extends LitElement {
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.apps.configuration.audio.header"
)}
.header=${this.supervisor.localize("addon.configuration.audio.header")}
>
<div class="card-content">
${this._error
@@ -51,8 +52,8 @@ class SupervisorAppAudio extends LitElement {
: nothing}
${this._inputDevices &&
html`<ha-select
.label=${this.hass.localize(
"ui.panel.config.apps.configuration.audio.input"
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
@@ -71,8 +72,8 @@ class SupervisorAppAudio extends LitElement {
</ha-select>`}
${this._outputDevices &&
html`<ha-select
.label=${this.hass.localize(
"ui.panel.config.apps.configuration.audio.output"
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
@@ -95,7 +96,7 @@ class SupervisorAppAudio extends LitElement {
.disabled=${this.disabled}
@click=${this._saveSettings}
>
${this.hass.localize("ui.common.save")}
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -105,7 +106,7 @@ class SupervisorAppAudio extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
:host,
ha-card {
@@ -118,7 +119,7 @@ class SupervisorAppAudio extends LitElement {
width: 100%;
}
ha-select:last-child {
margin-top: var(--ha-space-2);
margin-top: 8px;
}
`,
];
@@ -152,9 +153,7 @@ class SupervisorAppAudio extends LitElement {
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: this.hass.localize(
"ui.panel.config.apps.configuration.audio.default"
),
name: this.supervisor.localize("addon.configuration.audio.default"),
};
try {
@@ -171,9 +170,7 @@ class SupervisorAppAudio extends LitElement {
this._inputDevices = [noDevice, ...input];
this._outputDevices = [noDevice, ...output];
} catch {
this._error = this.hass.localize(
"ui.panel.config.apps.configuration.audio.failed_to_load_hardware"
);
this._error = "Failed to fetch audio hardware";
this._inputDevices = [noDevice];
this._outputDevices = [noDevice];
}
@@ -197,12 +194,10 @@ class SupervisorAppAudio extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch {
this._error = this.hass.localize(
"ui.panel.config.apps.configuration.audio.failed_to_save"
);
this._error = "Failed to set addon audio device";
}
button.progress = false;
@@ -211,6 +206,6 @@ class SupervisorAppAudio extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-audio": SupervisorAppAudio;
"hassio-addon-audio": HassioAddonAudio;
}
}

View File

@@ -1,20 +1,23 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../../components/ha-spinner";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import "../info/supervisor-app-system-managed";
import "./supervisor-app-audio";
import "./supervisor-app-config";
import "./supervisor-app-network";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../info/hassio-addon-system-managed";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-network";
@customElement("supervisor-app-config-tab")
class SupervisorAppConfigDashboard extends LitElement {
@customElement("hassio-addon-config-tab")
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean }) public narrow = false;
@@ -35,49 +38,50 @@ class SupervisorAppConfigDashboard extends LitElement {
${this.addon.system_managed &&
(hasConfiguration || this.addon.network || this.addon.audio)
? html`
<supervisor-app-system-managed
.hass=${this.hass}
<hassio-addon-system-managed
.supervisor=${this.supervisor}
.narrow=${this.narrow}
.hideButton=${this.controlEnabled}
></supervisor-app-system-managed>
></hassio-addon-system-managed>
`
: nothing}
${hasConfiguration || this.addon.network || this.addon.audio
? html`
${hasConfiguration
? html`
<supervisor-app-config
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></supervisor-app-config>
></hassio-addon-config>
`
: nothing}
${this.addon.network
? html`
<supervisor-app-network
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></supervisor-app-network>
></hassio-addon-network>
`
: nothing}
${this.addon.audio
? html`
<supervisor-app-audio
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
.disabled=${this.addon.system_managed &&
!this.controlEnabled}
></supervisor-app-audio>
></hassio-addon-audio>
`
: nothing}
`
: this.hass.localize(
"ui.panel.config.apps.configuration.no_configuration"
)}
: this.supervisor.localize("addon.configuration.no_configuration")}
</div>
`;
}
@@ -85,17 +89,17 @@ class SupervisorAppConfigDashboard extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: var(--ha-space-2);
padding: 8px;
max-width: 1024px;
}
supervisor-app-network,
supervisor-app-audio,
supervisor-app-config {
margin-bottom: var(--ha-space-6);
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
}
`,
];
@@ -104,6 +108,6 @@ class SupervisorAppConfigDashboard extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-config-tab": SupervisorAppConfigDashboard;
"hassio-addon-config-tab": HassioAddonConfigDashboard;
}
}

View File

@@ -1,41 +1,42 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiDotsVertical } from "@mdi/js";
import { DEFAULT_SCHEMA, Type } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-form/ha-form";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type {
HaFormDataContainer,
HaFormSchema,
} from "../../../../../components/ha-form/types";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
HaFormDataContainer,
} from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../../data/hassio/addon";
} from "../../../../src/data/hassio/addon";
import {
setHassioAddonOption,
validateHassioAddonOption,
} from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import type { ObjectSelector, Selector } from "../../../../../data/selector";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
const SUPPORTED_UI_TYPES = [
"string",
@@ -55,12 +56,14 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("supervisor-app-config")
class SupervisorAppConfig extends LitElement {
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public disabled = false;
@state() private _configHasChanged = false;
@@ -216,39 +219,31 @@ class SupervisorAppConfig extends LitElement {
<ha-card outlined>
<div class="header">
<h2>
${this.hass.localize(
"ui.panel.config.apps.configuration.options.header"
)}
${this.supervisor.localize("addon.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-dropdown @wa-select=${this._handleAction}>
<ha-button-menu @action=${this._handleAction}>
<ha-icon-button
.label=${this.hass.localize("ui.common.menu")}
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-dropdown-item
value="toggle_yaml"
.disabled=${!this._canShowSchema || this.disabled}
>
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.apps.configuration.options.edit_in_ui"
? this.supervisor.localize(
"addon.configuration.options.edit_in_ui"
)
: this.hass.localize(
"ui.panel.config.apps.configuration.options.edit_in_yaml"
: this.supervisor.localize(
"addon.configuration.options.edit_in_yaml"
)}
</ha-dropdown-item>
<ha-dropdown-item
value="reset"
.variant=${!this.disabled ? "danger" : "default"}
</ha-list-item>
<ha-list-item
class=${!this.disabled ? "warning" : ""}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.apps.configuration.reset_defaults"
)}
</ha-dropdown-item>
</ha-dropdown>
${this.supervisor.localize("common.reset_defaults")}
</ha-list-item>
</ha-button-menu>
</div>
</div>
@@ -283,8 +278,8 @@ class SupervisorAppConfig extends LitElement {
? ""
: html`
<ha-alert alert-type="error">
${this.hass.localize(
"ui.panel.config.apps.configuration.options.invalid_yaml"
${this.supervisor.localize(
"addon.configuration.options.invalid_yaml"
)}
</ha-alert>
`}
@@ -292,8 +287,8 @@ class SupervisorAppConfig extends LitElement {
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
.label=${this.hass.localize(
"ui.panel.config.apps.configuration.options.show_unused_optional"
.label=${this.supervisor.localize(
"addon.configuration.options.show_unused_optional"
)}
>
<ha-switch
@@ -310,7 +305,7 @@ class SupervisorAppConfig extends LitElement {
!this._configHasChanged ||
!this._valid}
>
${this.hass.localize("ui.common.save")}
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -319,13 +314,11 @@ class SupervisorAppConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema =
this.addon.schema !== null &&
!this.addon.schema!.find(
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
);
this._canShowSchema = !this.addon.schema!.find(
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
);
this._yamlMode = !this._canShowSchema;
}
@@ -347,19 +340,14 @@ class SupervisorAppConfig extends LitElement {
}
}
private _handleAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
if (!action) {
return;
}
switch (action) {
case "toggle_yaml":
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._yamlMode = !this._yamlMode;
return;
case "reset":
break;
case 1:
this._resetTapped(ev);
break;
}
}
@@ -383,14 +371,10 @@ class SupervisorAppConfig extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.configuration.confirm.reset_options.title"
),
text: this.hass.localize(
"ui.panel.config.apps.configuration.confirm.reset_options.text"
),
confirmText: this.hass.localize("ui.common.reset_options"),
dismissText: this.hass.localize("ui.common.cancel"),
title: this.supervisor.localize("confirm.reset_options.title"),
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
});
@@ -413,12 +397,9 @@ class SupervisorAppConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_reset",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_reset", {
error: extractApiErrorMessage(err),
});
}
button.progress = false;
}
@@ -456,15 +437,12 @@ class SupervisorAppConfig extends LitElement {
this._configHasChanged = false;
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
eventdata.success = false;
}
button.progress = false;
@@ -474,7 +452,7 @@ class SupervisorAppConfig extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
:host {
display: block;
@@ -492,7 +470,7 @@ class SupervisorAppConfig extends LitElement {
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
ha-dropdown-item[disabled] {
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
.header {
@@ -524,6 +502,6 @@ class SupervisorAppConfig extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-config": SupervisorAppConfig;
"hassio-addon-config": HassioAddonConfig;
}
}

View File

@@ -2,28 +2,31 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
} from "../../../../../data/hassio/addon";
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
} from "../../../../src/data/hassio/addon";
import { setHassioAddonOption } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("supervisor-app-network")
class SupervisorAppNetwork extends LitElement {
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ type: Boolean }) public disabled = false;
@@ -36,6 +39,11 @@ class SupervisorAppNetwork extends LitElement {
@state() private _config?: Record<string, any>;
public connectedCallback(): void {
super.connectedCallback();
this._setNetworkConfig();
}
protected render() {
if (!this._config) {
return nothing;
@@ -48,14 +56,14 @@ class SupervisorAppNetwork extends LitElement {
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.apps.configuration.network.header"
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.apps.configuration.network.introduction"
${this.supervisor.localize(
"addon.configuration.network.introduction"
)}
</p>
${this._error
@@ -78,8 +86,8 @@ class SupervisorAppNetwork extends LitElement {
${hasHiddenOptions
? html`<ha-formfield
class="show-optional"
.label=${this.hass.localize(
"ui.panel.config.apps.configuration.network.show_disabled"
.label=${this.supervisor.localize(
"addon.configuration.network.show_disabled"
)}
>
<ha-switch
@@ -96,23 +104,21 @@ class SupervisorAppNetwork extends LitElement {
.disabled=${this.disabled}
@click=${this._resetTapped}
>
${this.hass.localize(
"ui.panel.config.apps.configuration.network.reset_defaults"
)}
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || this.disabled}
>
${this.hass.localize("ui.common.save")}
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._setNetworkConfig();
}
@@ -178,15 +184,12 @@ class SupervisorAppNetwork extends LitElement {
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_reset",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_reset", {
error: extractApiErrorMessage(err),
});
button.actionError();
}
}
@@ -223,15 +226,12 @@ class SupervisorAppNetwork extends LitElement {
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
button.actionError();
}
}
@@ -239,7 +239,7 @@ class SupervisorAppNetwork extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
:host {
display: block;
@@ -261,6 +261,6 @@ class SupervisorAppNetwork extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-network": SupervisorAppNetwork;
"hassio-addon-network": HassioAddonNetwork;
}
}

View File

@@ -1,22 +1,25 @@
import "../../../../../components/ha-card";
import "../../../../src/components/ha-card";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-markdown";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-spinner";
import "../../../../src/components/ha-markdown";
import { customElement, property, state } from "lit/decorators";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import { fetchHassioAddonDocumentation } from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import "../../../../../layouts/hass-loading-screen";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { fetchHassioAddonDocumentation } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("supervisor-app-documentation-tab")
class SupervisorAppDocumentationDashboard extends LitElement {
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _error?: string;
@@ -54,18 +57,18 @@ class SupervisorAppDocumentationDashboard extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
ha-card {
display: block;
}
.content {
margin: auto;
padding: var(--ha-space-2);
padding: 8px;
max-width: 1024px;
}
ha-markdown {
padding: var(--ha-space-4);
padding: 16px;
}
`,
];
@@ -79,8 +82,8 @@ class SupervisorAppDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.documentation.get_documentation",
this._error = this.supervisor.localize(
"addon.documentation.get_documentation",
{ error: extractApiErrorMessage(err) }
);
}
@@ -89,6 +92,6 @@ class SupervisorAppDocumentationDashboard extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-documentation-tab": SupervisorAppDocumentationDashboard;
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
}
}

View File

@@ -0,0 +1,294 @@
import {
mdiCogs,
mdiFileDocument,
mdiInformationVariant,
mdiMathLog,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchAddonInfo,
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
import {
addStoreRepository,
fetchSupervisorStore,
} from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import "./config/hassio-addon-audio";
import "./config/hassio-addon-config";
import "./config/hassio-addon-network";
import "./hassio-addon-router";
import "./info/hassio-addon-info";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public addon?:
| HassioAddonDetails
| StoreAddonDetails;
@property({ type: Boolean }) public narrow = false;
@state()
private _controlEnabled = false;
@state() private _error?: string;
private _backPath = new URLSearchParams(window.parent.location.search).get(
"store"
)
? "/hassio/store"
: "/hassio/dashboard";
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
? {
prefix: route.prefix + route.path,
path: "",
}
: {
prefix: route.prefix + route.path.substr(0, dividerPos),
path: route.path.substr(dividerPos),
};
});
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
if (!this.addon || !this.supervisor?.addon) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const addonTabs: PageNavigation[] = [
{
translationKey: "addon.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
];
if (this.addon.documentation) {
addonTabs.push({
translationKey: "addon.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
}
if (this.addon.version) {
addonTabs.push(
{
translationKey: "addon.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
translationKey: "addon.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
}
);
}
const route = this._computeTail(this.route);
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${route}
.tabs=${addonTabs}
.backPath=${this._backPath}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
.route=${route}
.narrow=${this.narrow}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
.controlEnabled=${this._controlEnabled}
@system-managed-take-control=${this._enableControl}
></hassio-addon-router>
</hass-tabs-subpage>
`;
}
private _enableControl() {
this._controlEnabled = true;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url");
if (requestedAddonRepository) {
const storeInfo = await fetchSupervisorStore(this.hass);
if (
!storeInfo.repositories.find(
(repo) => repo.source === requestedAddonRepository
)
) {
if (
!(await showConfirmationDialog(this, {
title: this.supervisor.localize("my.add_addon_repository_title"),
text: this.supervisor.localize(
"my.add_addon_repository_description",
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) {
this._error = this.supervisor.localize(
"my.error_repository_not_found"
);
return;
}
try {
await addStoreRepository(this.hass, requestedAddonRepository);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
}
}
if (requestedAddon) {
const store = await fetchSupervisorStore(this.hass);
const validAddon = store.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
}
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
if (!ev.detail.success) {
return;
}
const pathSplit: string[] = ev.detail.path?.split("/");
if (!pathSplit || pathSplit.length === 0) {
return;
}
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
}
if (path === "uninstall") {
if (this.isConnected) {
navigate(this._backPath);
}
} else if (path === "install") {
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
} else {
await this._routeDataChanged();
}
}
protected updated(changedProperties) {
if (changedProperties.has("route") && !this.addon) {
this._routeDataChanged();
}
}
private async _routeDataChanged(): Promise<void> {
const addon = this.route.path.split("/")[1];
if (!addon) {
return;
}
try {
if (!this.supervisor.addon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
fireEvent(this, "supervisor-update", { addon: addonsInfo });
}
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
} catch (err: any) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-dashboard": HassioAddonDashboard;
}
}

View File

@@ -0,0 +1,62 @@
import { customElement, property } from "lit/decorators";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../../src/layouts/hass-router-page";
import type { HomeAssistant } from "../../../src/types";
import "./config/hassio-addon-config-tab";
import "./documentation/hassio-addon-documentation-tab";
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
import "./info/hassio-addon-info-tab";
import "./log/hassio-addon-log-tab";
@customElement("hassio-addon-router")
class HassioAddonRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!:
| HassioAddonDetails
| StoreAddonDetails;
@property({ type: Boolean, attribute: "control-enabled" })
public controlEnabled = false;
protected routerOptions: RouterOptions = {
defaultPage: "info",
showLoading: true,
routes: {
info: {
tag: "hassio-addon-info-tab",
},
documentation: {
tag: "hassio-addon-documentation-tab",
},
config: {
tag: "hassio-addon-config-tab",
},
logs: {
tag: "hassio-addon-log-tab",
},
},
};
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.supervisor = this.supervisor;
el.addon = this.addon;
el.narrow = this.narrow;
el.controlEnabled = this.controlEnabled;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-router": HassioAddonRouter;
}
}

View File

@@ -1,21 +1,24 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../../components/ha-spinner";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import "./supervisor-app-info";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info";
@customElement("supervisor-app-info-tab")
class SupervisorAppInfoDashboard extends LitElement {
@customElement("hassio-addon-info-tab")
class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean, attribute: "control-enabled" })
@@ -28,13 +31,14 @@ class SupervisorAppInfoDashboard extends LitElement {
return html`
<div class="content">
<supervisor-app-info
<hassio-addon-info
.narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
.controlEnabled=${this.controlEnabled}
></supervisor-app-info>
></hassio-addon-info>
</div>
`;
}
@@ -42,11 +46,11 @@ class SupervisorAppInfoDashboard extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: var(--ha-space-2);
padding: 8px;
max-width: 1024px;
}
`,
@@ -56,6 +60,6 @@ class SupervisorAppInfoDashboard extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-info-tab": SupervisorAppInfoDashboard;
"hassio-addon-info-tab": HassioAddonInfoDashboard;
}
}

View File

@@ -27,28 +27,28 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../../common/config/version";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { navigate } from "../../../../../common/navigate";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/chips/ha-assist-chip";
import "../../../../../components/chips/ha-chip-set";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-markdown";
import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
import { capitalizeFirstLetter } from "../../../../src/common/string/capitalize-first-letter";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/chips/ha-assist-chip";
import "../../../../src/components/chips/ha-chip-set";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import type { HaSwitch } from "../../../../src/components/ha-switch";
import type {
AddonCapability,
HassioAddonDetails,
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
} from "../../../../../data/hassio/addon";
} from "../../../../src/data/hassio/addon";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
@@ -61,26 +61,33 @@ import {
stopHassioAddon,
uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../../data/hassio/addon";
import type { HassioStats } from "../../../../../data/hassio/common";
} from "../../../../src/data/hassio/addon";
import type { HassioStats } from "../../../../src/data/hassio/common";
import {
extractApiErrorMessage,
fetchHassioStats,
} from "../../../../../data/hassio/common";
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
} from "../../../../src/data/hassio/common";
import type {
StoreAddon,
StoreAddonDetails,
} from "../../../../src/data/supervisor/store";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import { mdiHomeAssistant } from "../../../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { bytesToString } from "../../../../../util/bytes-to-string";
import "../../components/supervisor-apps-card-content";
import "../components/supervisor-app-metric";
import { extractChangelog } from "../util/supervisor-app";
import "./supervisor-app-system-managed";
import "../components/supervisor-app-update-available-card";
} from "../../../../src/dialogs/generic/show-dialog-box";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showSystemManagedDialog } from "../../dialogs/system-managed/show-dialog-system-managed";
import { hassioStyle } from "../../resources/hassio-style";
import "../../update-available/update-available-card";
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
import "./hassio-addon-system-managed";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -99,8 +106,8 @@ const RATING_ICON = {
8: mdiNumeric8,
};
@customElement("supervisor-app-info")
class SupervisorAppInfo extends LitElement {
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@@ -111,6 +118,8 @@ class SupervisorAppInfo extends LitElement {
| HassioAddonDetails
| StoreAddonDetails;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean, attribute: "control-enabled" })
public controlEnabled = false;
@@ -120,27 +129,32 @@ class SupervisorAppInfo extends LitElement {
private _fetchDataTimeout?: number;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
public disconnectedCallback() {
super.disconnectedCallback();
if (this._fetchDataTimeout) {
clearTimeout(this._fetchDataTimeout);
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
}
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
: undefined;
const metrics = [
{
description: this.hass.localize(
"ui.panel.config.apps.dashboard.cpu_usage"
),
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: this.hass.localize(
"ui.panel.config.apps.dashboard.ram_usage"
),
description: this.supervisor.localize("addon.dashboard.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -153,32 +167,33 @@ class SupervisorAppInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<supervisor-app-update-available-card
<update-available-card
.hass=${this.hass}
.narrow=${this.narrow}
.addon=${this.addon}
.supervisor=${this.supervisor}
.addonSlug=${this.addon.slug}
@update-complete=${this._updateComplete}
></supervisor-app-update-available-card>
></update-available-card>
`
: nothing}
${"protected" in this.addon && !this.addon.protected
? html`
<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.protection_mode.title"
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.protection_mode.content"
${this.supervisor.localize(
"addon.dashboard.protection_mode.content"
)}
<ha-button
variant="danger"
slot="action"
@click=${this._protectionToggled}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.protection_mode.enable"
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
</ha-button>
</ha-alert>
@@ -186,11 +201,11 @@ class SupervisorAppInfo extends LitElement {
: nothing}
${systemManaged
? html`
<supervisor-app-system-managed
.hass=${this.hass}
<hassio-addon-system-managed
.supervisor=${this.supervisor}
.narrow=${this.narrow}
.hideButton=${this.controlEnabled}
></supervisor-app-system-managed>
></hassio-addon-system-managed>
`
: nothing}
@@ -204,8 +219,8 @@ class SupervisorAppInfo extends LitElement {
${this._computeIsRunning
? html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_running"
.title=${this.supervisor.localize(
"dashboard.addon_running"
)}
class="running"
.path=${mdiPlayCircle}
@@ -213,8 +228,8 @@ class SupervisorAppInfo extends LitElement {
`
: html`
<ha-svg-icon
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.app_stopped"
.title=${this.supervisor.localize(
"dashboard.addon_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
@@ -227,21 +242,21 @@ class SupervisorAppInfo extends LitElement {
<div class="description light-color">
${this.addon.version
? html`
${this.hass.localize(
"ui.panel.config.apps.dashboard.current_version",
${this.supervisor.localize(
"addon.dashboard.current_version",
{ version: this.addon.version }
)}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link"
>${this.hass.localize(
"ui.panel.config.apps.dashboard.changelog"
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}
>${this.hass.localize(
"ui.panel.config.apps.dashboard.changelog"
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>`}
</div>
@@ -258,8 +273,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="stage"
.label=${capitalizeFirstLetter(
this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${this.addon.stage}`
this.supervisor.localize(
`addon.dashboard.capability.stages.${this.addon.stage}`
)
)}
>
@@ -277,13 +292,13 @@ class SupervisorAppInfo extends LitElement {
class=${classMap({
green: Number(this.addon.rating) >= 6,
yellow: [3, 4, 5].includes(Number(this.addon.rating)),
red: Number(this.addon.rating) <= 2,
red: Number(this.addon.rating) >= 2,
})}
@click=${this._showMoreInfo}
id="rating"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.rating"
this.supervisor.localize(
"addon.dashboard.capability.label.rating"
)
)}
>
@@ -297,8 +312,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="host_network"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.host"
this.supervisor.localize(
"addon.dashboard.capability.label.host"
)
)}
>
@@ -313,8 +328,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="full_access"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.hardware"
this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)
)}
>
@@ -329,8 +344,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="homeassistant_api"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.core"
this.supervisor.localize(
"addon.dashboard.capability.label.core"
)
)}
>
@@ -348,8 +363,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="hassio_api"
.label=${capitalizeFirstLetter(
this.hass.localize(
`ui.panel.config.apps.dashboard.capability.role.${this.addon.hassio_role}`
this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role
)}
>
@@ -367,8 +382,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="docker_api"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.docker"
this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)
)}
>
@@ -383,8 +398,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="host_pid"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.host_pid"
this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)
)}
>
@@ -400,8 +415,8 @@ class SupervisorAppInfo extends LitElement {
class=${this._computeApparmorClassName}
id="apparmor"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.apparmor"
this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)
)}
>
@@ -416,8 +431,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="auth_api"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.auth"
this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)
)}
>
@@ -432,8 +447,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="ingress"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.ingress"
this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)
)}
>
@@ -451,8 +466,8 @@ class SupervisorAppInfo extends LitElement {
@click=${this._showMoreInfo}
id="signed"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.capability.label.signed"
this.supervisor.localize(
"addon.dashboard.capability.label.signed"
)
)}
>
@@ -464,12 +479,10 @@ class SupervisorAppInfo extends LitElement {
? html`
<ha-assist-chip
filled
@click=${this._showSystemManagedInfo}
@click=${this._showSystemManagedDialog}
id="system_managed"
.label=${capitalizeFirstLetter(
this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.badge"
)
this.supervisor.localize("addon.system_managed.badge")
)}
>
<ha-svg-icon
@@ -483,17 +496,14 @@ class SupervisorAppInfo extends LitElement {
<div class="description light-color">
${this.addon.description}.<br />
${this.hass.localize(
"ui.panel.config.apps.dashboard.visit_app_page",
{
name: html`<a
href=${this.addon.url!}
target="_blank"
rel="noreferrer"
>${this.addon.name}</a
>`,
}
)}
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
name: html`<a
href=${this.addon.url!}
target="_blank"
rel="noreferrer"
>${this.addon.name}</a
>`,
})}
</div>
<div class="addon-container">
<div>
@@ -516,13 +526,13 @@ class SupervisorAppInfo extends LitElement {
>
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.boot.title"
${this.supervisor.localize(
"addon.dashboard.option.boot.title"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.boot.description"
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
</span>
<ha-switch
@@ -537,13 +547,13 @@ class SupervisorAppInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.watchdog.title"
${this.supervisor.localize(
"addon.dashboard.option.watchdog.title"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.watchdog.description"
${this.supervisor.localize(
"addon.dashboard.option.watchdog.description"
)}
</span>
<ha-switch
@@ -561,13 +571,13 @@ class SupervisorAppInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.auto_update.title"
${this.supervisor.localize(
"addon.dashboard.option.auto_update.title"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.auto_update.description"
${this.supervisor.localize(
"addon.dashboard.option.auto_update.description"
)}
</span>
<ha-switch
@@ -584,13 +594,13 @@ class SupervisorAppInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.ingress_panel.title"
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.title"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.ingress_panel.description"
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.description"
)}
</span>
<ha-switch
@@ -607,13 +617,13 @@ class SupervisorAppInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.protected.title"
${this.supervisor.localize(
"addon.dashboard.option.protected.title"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dashboard.option.protected.description"
${this.supervisor.localize(
"addon.dashboard.option.protected.description"
)}
</span>
<ha-switch
@@ -634,19 +644,17 @@ class SupervisorAppInfo extends LitElement {
${this.addon.version && this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.apps.dashboard.hostname"
)}
${this.supervisor.localize("addon.dashboard.hostname")}
</span>
<code slot="description"> ${this.addon.hostname} </code>
</ha-settings-row>
${metrics.map(
(metric) => html`
<supervisor-app-metric
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-app-metric>
></supervisor-metric>
`
)}`
: nothing}
@@ -655,6 +663,30 @@ class SupervisorAppInfo extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${!this.addon.version && addonStoreInfo && !this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</ha-alert>
`
: html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo!.homeassistant,
}
)}
</ha-alert>
`
: nothing}
</div>
<div class="card-actions">
<div>
@@ -667,19 +699,14 @@ class SupervisorAppInfo extends LitElement {
@click=${this._stopClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.stop"
)}
${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
variant="danger"
appearance="plain"
@click=${this._restartClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.restart"
)}
${this.supervisor.localize("addon.dashboard.restart")}
</ha-progress-button>
`
: html`
@@ -688,9 +715,7 @@ class SupervisorAppInfo extends LitElement {
.progress=${this.addon.state === "startup"}
appearance="plain"
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.start"
)}
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
: nothing}
@@ -704,9 +729,7 @@ class SupervisorAppInfo extends LitElement {
@click=${this._uninstallClicked}
.disabled=${systemManaged && !this.controlEnabled}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall"
)}
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
@@ -715,9 +738,7 @@ class SupervisorAppInfo extends LitElement {
appearance="plain"
@click=${this._rebuildClicked}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.rebuild"
)}
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-progress-button>
`
: nothing}
@@ -739,8 +760,8 @@ class SupervisorAppInfo extends LitElement {
? this._openIngress
: undefined}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.open_web_ui"
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</ha-button>
`
@@ -751,9 +772,7 @@ class SupervisorAppInfo extends LitElement {
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.install"
)}
${this.supervisor.localize("addon.dashboard.install")}
</ha-progress-button>
`}
</div>
@@ -785,7 +804,7 @@ class SupervisorAppInfo extends LitElement {
"state" in this.addon &&
this.addon.state === "startup"
) {
// App is starting up, wait for it to start
// Addon is starting up, wait for it to start
this._scheduleDataUpdate();
}
}
@@ -838,24 +857,28 @@ class SupervisorAppInfo extends LitElement {
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id as AddonCapability;
showAlertDialog(this, {
title: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.title`
),
text: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.${id}.description`
),
showHassioMarkdownDialog(this, {
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
content:
id === "stage"
? this.supervisor.localize(
`addon.dashboard.capability.${id}.description`,
{
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
icon_deprecated: `<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`,
}
)
: this.supervisor.localize(
`addon.dashboard.capability.${id}.description`
),
});
}
private _showSystemManagedInfo() {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.title"
),
text: this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.description"
),
private _showSystemManagedDialog() {
showSystemManagedDialog(this, {
addon: this.addon as HassioAddonDetails,
supervisor: this.supervisor,
});
}
@@ -879,7 +902,7 @@ class SupervisorAppInfo extends LitElement {
}
private _openIngress(): void {
navigate(`/app/${this.addon.slug}`);
navigate(`/hassio/ingress/${this.addon.slug}`);
}
private get _computeShowIngressUI(): boolean {
@@ -913,12 +936,9 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
}
@@ -936,12 +956,9 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
}
@@ -959,12 +976,9 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
}
@@ -982,12 +996,9 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
}
@@ -1005,12 +1016,9 @@ class SupervisorAppInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
this._error = this.hass.localize(
"ui.panel.config.apps.dashboard.failed_to_save",
{
error: extractApiErrorMessage(err),
}
);
this._error = this.supervisor.localize("addon.failed_to_save", {
error: extractApiErrorMessage(err),
});
}
}
@@ -1021,19 +1029,14 @@ class SupervisorAppInfo extends LitElement {
this.addon.slug
);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.apps.dashboard.changelog"),
text: html`<ha-markdown
.content=${extractChangelog(
this.addon as HassioAddonDetails,
content
)}
></ha-markdown>`,
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"),
content: extractChangelog(this.addon as HassioAddonDetails, content),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.get_changelog"
title: this.supervisor.localize(
"addon.dashboard.action_error.get_changelog"
),
text: extractApiErrorMessage(err),
});
@@ -1063,9 +1066,7 @@ class SupervisorAppInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.install"
),
title: this.supervisor.localize("addon.dashboard.action_error.install"),
text: extractApiErrorMessage(err),
});
}
@@ -1090,9 +1091,7 @@ class SupervisorAppInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.stop"
),
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
text: extractApiErrorMessage(err),
});
}
@@ -1100,10 +1099,6 @@ class SupervisorAppInfo extends LitElement {
}
private async _restartClicked(ev: CustomEvent): Promise<void> {
if (this._isSystemManaged(this.addon) && !this.controlEnabled) {
return;
}
const button = ev.currentTarget as any;
button.progress = true;
@@ -1112,14 +1107,12 @@ class SupervisorAppInfo extends LitElement {
const eventdata = {
success: true,
response: undefined,
path: "restart",
path: "stop",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.restart"
),
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
text: extractApiErrorMessage(err),
});
}
@@ -1134,9 +1127,7 @@ class SupervisorAppInfo extends LitElement {
await rebuildLocalAddon(this.hass, this.addon.slug);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.rebuild"
),
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
text: extractApiErrorMessage(err),
});
}
@@ -1153,15 +1144,15 @@ class SupervisorAppInfo extends LitElement {
);
if (!validate.valid) {
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.start_invalid_config"
title: this.supervisor.localize(
"addon.dashboard.action_error.start_invalid_config"
),
text: validate.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.go_to_config"
confirmText: this.supervisor.localize(
"addon.dashboard.action_error.go_to_config"
),
dismissText: this.hass.localize("ui.common.cancel"),
dismissText: this.supervisor.localize("common.cancel"),
});
button.actionError();
button.progress = false;
@@ -1171,9 +1162,7 @@ class SupervisorAppInfo extends LitElement {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.validate_config"
),
title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err),
});
return;
@@ -1192,9 +1181,7 @@ class SupervisorAppInfo extends LitElement {
button.actionError();
button.progress = false;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.start"
),
title: this.supervisor.localize("addon.dashboard.action_error.start"),
text: extractApiErrorMessage(err),
});
return;
@@ -1204,7 +1191,7 @@ class SupervisorAppInfo extends LitElement {
}
private _openConfiguration(): void {
navigate(`/config/app/${this.addon.slug}/config`);
navigate(`/hassio/addon/${this.addon.slug}/config`);
}
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
@@ -1220,18 +1207,13 @@ class SupervisorAppInfo extends LitElement {
};
const confirmed = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall_dialog.title",
{
name: this.addon.name,
}
),
title: this.supervisor.localize("dialog.uninstall_addon.title", {
name: this.addon.name,
}),
text: html`
<ha-formfield
.label=${html`<p>
${this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall_dialog.remove_data"
)}
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
</p>`}
>
<ha-switch
@@ -1241,10 +1223,8 @@ class SupervisorAppInfo extends LitElement {
></ha-switch>
</ha-formfield>
`,
confirmText: this.hass.localize(
"ui.panel.config.apps.dashboard.uninstall_dialog.uninstall"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
});
@@ -1265,8 +1245,8 @@ class SupervisorAppInfo extends LitElement {
button.actionSuccess();
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.action_error.uninstall"
title: this.supervisor.localize(
"addon.dashboard.action_error.uninstall"
),
text: extractApiErrorMessage(err),
});
@@ -1283,6 +1263,7 @@ class SupervisorAppInfo extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
@@ -1312,8 +1293,8 @@ class SupervisorAppInfo extends LitElement {
color: var(--secondary-text-color);
}
.addon-header {
padding-left: var(--ha-space-2);
padding-inline-start: var(--ha-space-2);
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: var(--ha-font-size-2xl);
color: var(--ha-card-header-color, var(--primary-text-color));
@@ -1403,11 +1384,6 @@ class SupervisorAppInfo extends LitElement {
}
ha-markdown {
padding: 16px;
--markdown-image-background-color: transparent;
--markdown-image-border-radius: 0;
--markdown-image-min-height: auto;
--markdown-image-text-indent: 0;
--markdown-image-transition: none;
}
ha-settings-row {
padding: 0;
@@ -1449,7 +1425,7 @@ class SupervisorAppInfo extends LitElement {
text-decoration: none;
}
supervisor-app-update-available-card {
update-available-card {
padding-bottom: 16px;
}
@@ -1467,6 +1443,6 @@ class SupervisorAppInfo extends LitElement {
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-info": SupervisorAppInfo;
"hassio-addon-info": HassioAddonInfo;
}
}

View File

@@ -1,16 +1,16 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import type { HomeAssistant } from "../../../../../types";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("supervisor-app-system-managed")
class SupervisorAppSystemManaged extends LitElement {
@customElement("hassio-addon-system-managed")
class HassioAddonSystemManaged extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean, attribute: "hide-button" }) public hideButton =
false;
@@ -19,20 +19,14 @@ class SupervisorAppSystemManaged extends LitElement {
return html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.title"
)}
.title=${this.supervisor.localize("addon.system_managed.title")}
.narrow=${this.narrow}
>
${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.description"
)}
${this.supervisor.localize("addon.system_managed.description")}
${!this.hideButton
? html`
<ha-button slot="action" @click=${this._takeControl}>
${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.take_control"
)}
${this.supervisor.localize("addon.system_managed.take_control")}
</ha-button>
`
: nothing}
@@ -56,7 +50,7 @@ class SupervisorAppSystemManaged extends LitElement {
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-system-managed": SupervisorAppSystemManaged;
"hassio-addon-system-managed": HassioAddonSystemManaged;
}
interface HASSDomEvents {

View File

@@ -6,19 +6,22 @@ import {
type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-spinner";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import "../../../logs/error-log-card";
import "../../../../../components/search-input";
import { extractSearchParam } from "../../../../../common/url/search-params";
import "../../../../src/components/ha-spinner";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/panels/config/logs/error-log-card";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
@customElement("supervisor-app-log-tab")
class SupervisorAppLogDashboard extends LitElement {
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
@@ -33,12 +36,13 @@ class SupervisorAppLogDashboard extends LitElement {
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
.label=${this.supervisor.localize("ui.panel.config.logs.search")}
></search-input>
</div>
<div class="content">
<error-log-card
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.header=${this.addon.name}
.provider=${this.addon.slug}
.filter=${this._filter}
@@ -55,11 +59,11 @@ class SupervisorAppLogDashboard extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
supervisorAppsStyle,
hassioStyle,
css`
.content {
margin: auto;
padding: var(--ha-space-2);
padding: 8px;
}
.search {
position: sticky;
@@ -83,6 +87,6 @@ class SupervisorAppLogDashboard extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-log-tab": SupervisorAppLogDashboard;
"hassio-addon-log-tab": HassioAddonLogDashboard;
}
}

View File

@@ -0,0 +1,425 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { relativeTime } from "../../../src/common/datetime/relative_time";
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
} from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/ha-svg-icon";
import type { HassioBackup } from "../../../src/data/hassio/backup";
import {
fetchHassioBackups,
friendlyFolderName,
reloadHassioBackups,
removeBackup,
} from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
type BackupItem = HassioBackup & {
secondary: string;
};
@customElement("hassio-backups")
export class HassioBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@state() private _selectedBackups: string[] = [];
@state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _firstUpdatedCalled = false;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchBackups();
}
}
private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup");
}
const content: string[] = [];
if (backup.content.homeassistant) {
content.push("Home Assistant");
}
if (backup.content.folders.length !== 0) {
for (const folder of backup.content.folders) {
content.push(friendlyFolderName[folder] || folder);
}
}
if (backup.content.addons.length !== 0) {
for (const addon of backup.content.addons) {
content.push(
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
?.name || addon
);
}
}
return content.join(", ");
};
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this._fetchBackups();
}
this._firstUpdatedCalled = true;
}
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
name: {
title: this.supervisor.localize("backup.name"),
main: true,
sortable: true,
filterable: true,
flex: 2,
template: (backup) =>
html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`,
},
size: {
title: this.supervisor.localize("backup.size"),
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
},
location: {
title: this.supervisor.localize("backup.location"),
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) =>
backup.location || this.supervisor.localize("backup.data_disk"),
},
date: {
title: this.supervisor.localize("backup.created"),
direction: "desc",
hidden: narrow,
filterable: true,
sortable: true,
template: (backup) =>
relativeTime(new Date(backup.date), this.hass.locale),
},
secondary: {
title: "",
hidden: true,
filterable: true,
},
})
);
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
backups.map((backup) => ({
...backup,
secondary: this._computeBackupContent(backup),
}))
);
protected render() {
if (!this.supervisor) {
return nothing;
}
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
? [
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
]
: supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("backup.search")}
.noDataText=${this.supervisor.localize("backup.no_backups")}
.narrow=${this.narrow}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.data=${this._backupData(this._backups || [])}
id="slug"
@row-click=${this._handleRowClicked}
@selection-changed=${this._handleSelectionChanged}
clickable
selectable
has-fab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
: "/config"}
supervisor
>
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
<ha-icon-button
.label=${this.supervisor?.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item>
${this.supervisor.localize("common.reload")}
</ha-list-item>
<ha-list-item>
${this.supervisor.localize("dialog.backup_location.title")}
</ha-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<ha-list-item>
${this.supervisor.localize("backup.upload_backup")}
</ha-list-item>`
: ""}
</ha-button-menu>
${this._selectedBackups.length
? html`<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.supervisor.localize("backup.selected", {
number: this._selectedBackups.length,
})}
</p>
<div class="header-btns">
${!this.narrow
? html`
<ha-button
appearance="plain"
variant="danger"
@click=${this._deleteSelected}
>
${this.supervisor.localize("backup.delete_selected")}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.supervisor.localize(
"backup.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`}
</div>
</div> `
: ""}
<ha-fab
slot="fab"
@click=${this._createBackup}
.label=${this.supervisor.localize("backup.create_backup")}
extended
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._fetchBackups();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
break;
case 2:
this._showUploadBackupDialog();
break;
}
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selectedBackups = ev.detail.value;
}
private _showUploadBackupDialog() {
showBackupUploadDialog(this, {
showBackup: (slug: string) =>
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
}),
reloadBackup: () => this._fetchBackups(),
});
}
private async _fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
}
private async _deleteSelected() {
const confirm = await showConfirmationDialog(this, {
title: this.supervisor.localize("backup.delete_backup_title"),
text: this.supervisor.localize("backup.delete_backup_text", {
number: this._selectedBackups.length,
}),
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
destructive: true,
});
if (!confirm) {
return;
}
try {
await Promise.all(
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("backup.failed_to_delete"),
text: extractApiErrorMessage(err),
});
return;
}
await this._fetchBackups();
this._dataTable.clearSelection();
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const slug = ev.detail.id;
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
});
}
private _createBackup() {
if (this.supervisor!.info.state !== "running") {
showAlertDialog(this, {
title: this.supervisor!.localize("backup.could_not_create"),
text: this.supervisor!.localize("backup.create_blocked_not_running", {
state: this.supervisor!.info.state,
}),
});
return;
}
showHassioCreateBackupDialog(this, {
supervisor: this.supervisor!,
onCreate: () => this._fetchBackups(),
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 58px;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: var(--ha-font-weight-bold);
padding-left: 16px;
padding-inline-start: 16px;
padding-inline-end: initial;
color: var(--primary-text-color);
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: var(--ha-font-size-l);
}
.header-toolbar .header-btns {
margin-right: -12px;
margin-inline-end: -12px;
margin-inline-start: initial;
}
.header-btns > ha-button,
.header-btns > ha-icon-button {
margin: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-backups": HassioBackups;
}
}

View File

@@ -2,11 +2,11 @@ import { mdiHelpCircle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types";
import "../../../src/components/ha-svg-icon";
import type { HomeAssistant } from "../../../src/types";
@customElement("supervisor-apps-card-content")
class SupervisorAppsCardContent extends LitElement {
@customElement("hassio-card-content")
class HassioCardContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@@ -70,9 +70,9 @@ class SupervisorAppsCardContent extends LitElement {
}
ha-svg-icon {
margin-right: var(--ha-space-6);
margin-left: var(--ha-space-2);
margin-top: var(--ha-space-3);
margin-right: 24px;
margin-left: 8px;
margin-top: 12px;
float: left;
color: var(--secondary-text-color);
}
@@ -106,8 +106,8 @@ class SupervisorAppsCardContent extends LitElement {
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: var(--ha-space-1);
margin-right: var(--ha-space-4);
margin-top: 4px;
margin-right: 16px;
float: left;
}
.icon_image.stopped,
@@ -119,8 +119,8 @@ class SupervisorAppsCardContent extends LitElement {
background-color: var(--warning-color);
width: 12px;
height: 12px;
top: var(--ha-space-2);
right: var(--ha-space-2);
top: 8px;
right: 8px;
border-radius: var(--ha-border-radius-circle);
}
.topbar {
@@ -146,6 +146,6 @@ class SupervisorAppsCardContent extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-apps-card-content": SupervisorAppsCardContent;
"hassio-card-content": HassioCardContent;
}
}

View File

@@ -1,6 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { StoreAddon } from "../../../../data/supervisor/store";
import type { StoreAddon } from "../../../src/data/supervisor/store";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {

View File

@@ -0,0 +1,89 @@
import { mdiFolderUpload } from "@mdi/js";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-file-upload";
import type { HassioBackup } from "../../../src/data/hassio/backup";
import { uploadBackup } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../src/types";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"hassio-backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@customElement("hassio-upload-backup")
export class HassioUploadBackup extends LitElement {
public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() public value: string | null = null;
@state() private _uploading = false;
public render(): TemplateResult {
return html`
<ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
accept="application/x-tar"
.label=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
.supports=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_supports"
) || "Supports .TAR files"}
.secondary=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_drop"
) || "Or drop your file here"}
@file-picked=${this._uploadFile}
@files-cleared=${this._clear}
></ha-file-upload>
`;
}
private _clear() {
this.value = null;
fireEvent(this, "backup-cleared");
}
private async _uploadFile(ev) {
const file = ev.detail.files[0];
if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, {
title: "Unsupported file format",
text: "Please choose a Home Assistant backup file (.tar)",
confirmText: "ok",
});
return;
}
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",
text: extractApiErrorMessage(err),
confirmText: "ok",
});
} finally {
this._uploading = false;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-upload-backup": HassioUploadBackup;
}
}

View File

@@ -0,0 +1,460 @@
import { mdiFolder, mdiPuzzle } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import type {
HassioBackupDetail,
HassioFullBackupCreateParams,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
interface CheckboxItem {
slug: string;
checked: boolean;
name: string;
}
interface AddonCheckboxItem extends CheckboxItem {
version: string;
}
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false });
}
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
}
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
};
const _computeAddons = (addons): AddonCheckboxItem[] =>
addons
.map((addon) => ({
slug: addon.slug,
name: addon.name,
version: addon.version,
checked: false,
}))
.sort((a, b) => (a.name > b.name ? 1 : -1));
@customElement("supervisor-backup-content")
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@property({ attribute: false })
public backupType: HassioBackupDetail["type"] = "full";
@property({ attribute: false }) public folders?: CheckboxItem[];
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
@property({ attribute: false }) public homeAssistant = false;
@property({ attribute: false }) public backupHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property({ attribute: false }) public backupName = "";
@property({ attribute: false }) public backupPassword = "";
@property({ attribute: false }) public confirmBackupPassword = "";
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this.folders = _computeFolders(
this.backup
? this.backup.folders
: ["ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.addon.addons
);
this.backupType = this.backup?.type || "full";
this.backupName = this.backup?.name || "";
this.backupHasPassword = this.backup?.protected || false;
}
}
public override focus() {
this._focusTarget?.focus();
}
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
}
const foldersSection =
this.backupType === "partial" ? this._getSection("folders") : undefined;
const addonsSection =
this.backupType === "partial" ? this._getSection("addons") : undefined;
return html`
${this.backup
? html`<div class="details">
${this.backup.type === "full"
? this.supervisor?.localize("backup.full_backup")
: this.supervisor?.localize("backup.partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(
new Date(this.backup.date),
this.hass.locale,
this.hass.config
)
: this.backup.date}
</div>`
: html`<ha-textfield
name="backupName"
.label=${this.supervisor?.localize("backup.name")}
.value=${this.backupName}
@change=${this._handleTextValueChanged}
>
</ha-textfield>`}
${!this.backup || this.backup.type === "full"
? html`<div class="sub-header">
${!this.backup
? this.supervisor?.localize("backup.type")
: this.supervisor?.localize("backup.select_type")}
</div>
<div class="backup-types">
<ha-formfield
.label=${this.supervisor?.localize("backup.full_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
name="backupType"
.checked=${this.backupType === "full"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor?.localize("backup.partial_backup")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
name="backupType"
.checked=${this.backupType === "partial"}
>
</ha-radio>
</ha-formfield>
</div>`
: ""}
${this.backupType === "partial"
? html`<div class="partial-picker">
${!this.backup || this.backup.homeassistant
? html`<ha-formfield
.label=${html`<supervisor-formfield-label
label="Home Assistant"
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass?.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.onboarding || this.homeAssistant}
.disabled=${this.onboarding}
@change=${this._toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${foldersSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${foldersSection.checked}
.indeterminate=${foldersSection.indeterminate}
.section=${"folders"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${foldersSection.templates}</div>
`
: ""}
${addonsSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
@change=${this._toggleSection}
.checked=${addonsSection.checked}
.indeterminate=${addonsSection.indeterminate}
.section=${"addons"}
>
</ha-checkbox>
</ha-formfield>
<div class="section-content">${addonsSection.templates}</div>
`
: ""}
</div> `
: ""}
${this.backupType === "partial" &&
(!this.backup || this.backupHasPassword)
? html`<hr />`
: ""}
${!this.backup
? html`<ha-formfield
class="password"
.label=${this.supervisor?.localize("backup.password_protection")}
>
<ha-checkbox
.checked=${this.backupHasPassword}
@change=${this._toggleHasPassword}
>
</ha-checkbox>
</ha-formfield>`
: ""}
${this.backupHasPassword
? html`
<ha-password-field
.label=${this.supervisor?.localize("backup.password")}
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>
${!this.backup
? html`<ha-password-field
.label=${this.supervisor?.localize("backup.confirm_password")}
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>`
: ""}
`
: ""}
`;
}
private _toggleHomeAssistant() {
this.homeAssistant = !this.homeAssistant;
}
static styles = css`
.partial-picker ha-formfield {
display: block;
}
.partial-picker ha-checkbox {
--mdc-checkbox-touch-target-size: 32px;
}
.partial-picker {
display: block;
margin: 0px -6px;
}
supervisor-formfield-label {
display: inline-flex;
align-items: center;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
.details {
color: var(--secondary-text-color);
}
.section-content {
display: flex;
flex-direction: column;
margin-left: 30px;
margin-inline-start: 30px;
margin-inline-end: initial;
}
ha-formfield.password {
display: block;
margin: 0 -14px -16px;
}
.backup-types {
display: flex;
margin-left: -13px;
margin-inline-start: -13px;
margin-inline-end: initial;
}
.sub-header {
margin-top: 8px;
}
`;
public backupDetails():
| HassioPartialBackupCreateParams
| HassioFullBackupCreateParams {
const data: any = {};
if (!this.backup && this.hass) {
data.name =
this.backupName ||
formatDate(new Date(), this.hass.locale, this.hass.config);
}
if (this.backupHasPassword) {
data.password = this.backupPassword;
if (!this.backup) {
data.confirm_password = this.confirmBackupPassword;
}
}
if (this.backupType === "full") {
return data;
}
const addons = this.addons
?.filter((addon) => addon.checked)
.map((addon) => addon.slug);
const folders = this.folders
?.filter((folder) => folder.checked)
.map((folder) => folder.slug);
if (addons?.length) {
data.addons = addons;
}
if (folders?.length) {
data.folders = folders;
}
// onboarding needs at least homeassistant to restore
data.homeassistant = this.onboarding || this.homeAssistant;
return data;
}
private _getSection(section: string) {
const templates: TemplateResult[] = [];
const addons =
section === "addons"
? new Map(
this.supervisor?.addon.addons.map((item) => [item.slug, item])
)
: undefined;
let checkedItems = 0;
this[section].forEach((item) => {
templates.push(
html`<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" &&
!this.onboarding &&
this.hass &&
atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`
: undefined}
.version=${item.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.item=${item}
.checked=${item.checked}
.section=${section}
@change=${this._updateSectionEntry}
>
</ha-checkbox>
</ha-formfield>`
);
if (item.checked) {
checkedItems++;
}
});
const checked = checkedItems === this[section].length;
return {
templates,
checked,
indeterminate: !checked && checkedItems !== 0,
};
}
private _handleRadioValueChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
this[input.name] = input.value;
}
private _handleTextValueChanged(ev: InputEvent) {
const input = ev.currentTarget as HaTextField;
this[input.name!] = input.value;
}
private _toggleHasPassword(): void {
this.backupHasPassword = !this.backupHasPassword;
}
private _toggleSection(ev): void {
const section = ev.currentTarget.section;
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
(item) => ({
...item,
checked: ev.currentTarget.checked,
})
);
}
private _updateSectionEntry(ev): void {
const item = ev.currentTarget.item;
const section = ev.currentTarget.section;
this[section] = this[section].map((entry) =>
entry.slug === item.slug
? {
...entry,
checked: ev.currentTarget.checked,
}
: entry
);
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-backup-content": SupervisorBackupContent;
}
}

View File

@@ -0,0 +1,60 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-svg-icon";
@customElement("supervisor-formfield-label")
class SupervisorFormfieldLabel extends LitElement {
@property({ type: String }) public label!: string;
@property({ attribute: false }) public imageUrl?: string;
@property({ attribute: false }) public iconPath?: string;
@property({ type: String }) public version?: string;
protected render(): TemplateResult {
return html`
${this.imageUrl
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
: this.iconPath
? html`<ha-svg-icon
.path=${this.iconPath}
class="icon"
></ha-svg-icon>`
: ""}
<span class="label">${this.label}</span>
${this.version
? html`<span class="version">(${this.version})</span>`
: ""}
`;
}
static styles = css`
:host {
display: flex;
align-items: center;
}
.label {
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
}
.version {
color: var(--secondary-text-color);
}
.icon {
max-height: 22px;
max-width: 22px;
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-formfield-label": SupervisorFormfieldLabel;
}
}

View File

@@ -2,12 +2,12 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../../components/ha-bar";
import "../../../../../components/ha-settings-row";
import { roundWithOneDecimal } from "../../../../../util/calculate";
import "../../../src/components/ha-bar";
import "../../../src/components/ha-settings-row";
import { roundWithOneDecimal } from "../../../src/util/calculate";
@customElement("supervisor-app-metric")
class SupervisorAppMetric extends LitElement {
@customElement("supervisor-metric")
class SupervisorMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public description!: string;
@@ -70,6 +70,6 @@ class SupervisorAppMetric extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-metric": SupervisorAppMetric;
"supervisor-metric": SupervisorMetric;
}
}

View File

@@ -0,0 +1,164 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card";
import "../../../src/components/search-input";
import type { HassioAddonInfo } from "../../../src/data/hassio/addon";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-addons")
class HassioAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@state() private _filter?: string;
protected render(): TemplateResult {
return html`
<div class="search">
<search-input
.hass=${this.hass}
suffix
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.supervisor.localize("dashboard.search_addons")}
>
</search-input>
</div>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
: ""}
<div class="card-group">
${!this.supervisor.addon.addons.length
? html`
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")}
</button>
</div>
</ha-card>
`
: this._getAddons(this.supervisor.addon.addons, this._filter).map(
(addon) => html`
<ha-card outlined .addon=${addon} @click=${this._addonTapped}>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.update_available!
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? this.supervisor.localize("dashboard.addon_stopped")
: addon.update_available!
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(
this.hass.config.version,
0,
105
) && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></hassio-card-content>
</div>
</ha-card>
`
)}
</div>
</div>
`;
}
private _getAddons = memoizeOne(
(addons: HassioAddonInfo[], filter?: string) => {
if (filter) {
addons = addons.filter((addon) => {
const lowerCaseFilter = filter.toLowerCase();
return (
addon.name.toLowerCase().includes(lowerCaseFilter) ||
addon.description.toLowerCase().includes(lowerCaseFilter) ||
addon.slug.toLowerCase().includes(lowerCaseFilter)
);
});
}
return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
);
}
);
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
cursor: pointer;
overflow: hidden;
direction: ltr;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
.content {
margin-bottom: 72px;
}
`,
];
}
private _addonTapped(ev: any): void {
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
}
private _openStore(): void {
navigate("/hassio/store");
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addons": HassioAddons;
}
}

View File

@@ -0,0 +1,150 @@
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-fab";
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
firstUpdated() {
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
import("./hassio-update");
}
}
protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.header=${this.supervisor.localize("panel.addons")}
>
<ha-icon-button
slot="toolbar-icon"
@click=${this._handleCheckUpdates}
.path=${mdiRefresh}
.label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
.narrow=${this.narrow}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
.label=${this.supervisor.localize("panel.store")}
extended
class="non-tabs"
>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon></ha-fab
></a>
</hass-subpage>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs(this.hass)}
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
has-fab
>
<span slot="header">
${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard"
)}
</span>
<div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
`
: ""}
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
</div>
<a href="/hassio/store" slot="fab">
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-tabs-subpage>
`;
}
private async _handleCheckUpdates() {
try {
await reloadHassioAddons(this.hass);
} catch (err) {
showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
} finally {
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.content {
margin: 0 auto;
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + var(--safe-area-inset-right));
bottom: calc(16px + var(--safe-area-inset-bottom));
inset-inline-end: calc(16px + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-dashboard": HassioDashboard;
}
}

View File

@@ -0,0 +1,158 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
import type {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string =>
key === "os" ? version : `${key}-${version}`;
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
private _pendingUpdates = memoizeOne(
(supervisor: Supervisor): number =>
Object.keys(supervisor).filter(
(value) => supervisor[value].update_available
).length
);
protected render() {
if (!this.supervisor) {
return nothing;
}
const updatesAvailable = this._pendingUpdates(this.supervisor);
if (!updatesAvailable) {
return nothing;
}
return html`
<div class="content">
<h1>
${this.supervisor.localize("common.update_available", {
count: updatesAvailable,
})}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core
)}
${this._renderUpdateCard(
"Supervisor",
"supervisor",
this.supervisor.supervisor
)}
${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard(
"Operating System",
"os",
this.supervisor.os
)
: ""}
</div>
</div>
`;
}
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
) {
if (!object.update_available) {
return nothing;
}
return html`
<ha-card outlined>
<div class="card-content">
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/hassio/update-available/${key}">
${this.supervisor.localize("common.show")}
</ha-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
.icon {
--mdc-icon-size: 48px;
float: right;
margin: 0 0 2px 10px;
color: var(--primary-text-color);
}
.update-heading {
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
}
.card-actions {
text-align: right;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-update": HassioUpdate;
}
}

View File

@@ -0,0 +1,155 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { changeMountOptions } from "../../../../src/data/supervisor/mounts";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location";
const SCHEMA = memoizeOne(
() =>
[
{
name: "default_backup_mount",
required: true,
selector: { backup_location: {} },
},
] as const
);
@customElement("dialog-hassio-backup-location")
class HassioBackupLocationDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioBackupLocationDialogParams;
@state() private _data?: { default_backup_mount: string | null };
@state() private _waiting?: boolean;
@state() private _error?: string;
public async showDialog(
dialogParams: HassioBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
}
public closeDialog(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this._dialogParams.supervisor.localize(
"dialog.backup_location.title"
)}
@closed=${this.closeDialog}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA()}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this._dialogParams.supervisor.localize("common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this._dialogParams.supervisor.localize("common.save")}
</ha-button>
</ha-dialog>
`;
}
private _computeLabelCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.name`
) || schema.name;
private _computeHelperCallback = (
// @ts-ignore
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
): string =>
this._dialogParams!.supervisor.localize(
`dialog.backup_location.options.${schema.name}.description`
);
private _valueChanged(ev: CustomEvent) {
const newLocation = ev.detail.value.default_backup_mount;
this._data = {
default_backup_mount: newLocation === "/backup" ? null : newLocation,
};
}
private async _changeMount() {
if (!this._data) {
return;
}
this._error = undefined;
this._waiting = true;
try {
await changeMountOptions(this.hass, this._data);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._waiting = false;
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.delete-btn {
--mdc-theme-primary: var(--error-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup-location": HassioBackupLocationDialog;
}
}

View File

@@ -0,0 +1,113 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-dialog";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-upload-backup";
import type { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload";
@customElement("dialog-hassio-backup-upload")
export class DialogHassioBackupUpload
extends LitElement
implements HassDialog<HassioBackupUploadDialogParams>
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _dialogParams?: HassioBackupUploadDialogParams;
public async showDialog(
dialogParams: HassioBackupUploadDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
await this.updateComplete;
}
public closeDialog() {
if (this._dialogParams && !this._dialogParams.onboarding) {
if (this._dialogParams.reloadBackup) {
this._dialogParams.reloadBackup();
}
}
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
.heading=${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title"
>${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}</span
>
<ha-icon-button
.label=${this.hass?.localize("ui.common.close") || "Close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
dialogInitialFocus
></ha-icon-button>
</ha-header-bar>
</div>
<hassio-upload-backup
@hassio-backup-uploaded=${this._backupUploaded}
.hass=${this.hass}
></hassio-upload-backup>
</ha-dialog>
`;
}
private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._dialogParams?.showBackup(backup.slug);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup-upload": DialogHassioBackupUpload;
}
}

View File

@@ -0,0 +1,339 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-md-dialog";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-spinner";
import { getSignedPath } from "../../../../src/data/auth";
import type { HassioBackupDetail } from "../../../../src/data/hassio/backup";
import {
fetchHassioBackupInfo,
removeBackup,
restoreBackup,
} from "../../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
@customElement("dialog-hassio-backup")
class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string;
@state() private _backup?: HassioBackupDetail;
@state() private _dialogParams?: HassioBackupDialogParams;
@state() private _restoringBackup = false;
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(dialogParams: HassioBackupDialogParams) {
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._dialogParams.supervisor?.localize(
"backup.no_backup_found"
);
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._dialogParams.supervisor?.localize(
"backup.restore_no_home_assistant"
);
}
this._restoringBackup = false;
}
private _dialogClosed(): void {
this._backup = undefined;
this._dialogParams = undefined;
this._restoringBackup = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog() {
this._dialog?.close();
return true;
}
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
}
return html`
<ha-md-dialog
open
.disableCancelAction=${!this._error}
@closed=${this._dialogClosed}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._dialogParams.supervisor?.localize("backup.close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
></ha-icon-button>
<span slot="title" .title=${this._backup.name}
>${this._backup.name}</span
>
${!this._dialogParams.onboarding && this._dialogParams.supervisor
? html`<ha-button-menu
slot="actionItems"
fixed
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize(
"backup.more_actions"
)}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item
>${this._dialogParams.supervisor.localize(
"backup.download_backup"
)}</ha-list-item
>
<ha-list-item class="error"
>${this._dialogParams.supervisor.localize(
"backup.delete_backup_title"
)}</ha-list-item
>
</ha-button-menu>`
: nothing}
</ha-dialog-header>
<div slot="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._restoringBackup
? html`<div class="loading">
<ha-spinner></ha-spinner>
</div>`
: html`
<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
dialogInitialFocus
>
</supervisor-backup-content>
`}
</div>
<div slot="actions">
<ha-button
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._dialogParams.supervisor?.localize("backup.restore")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._downloadClicked();
break;
case 1:
this._deleteClicked();
break;
}
}
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
const supervisor = this._dialogParams?.supervisor;
if (supervisor !== undefined && supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: supervisor.localize("backup.could_not_restore"),
text: supervisor.localize("backup.restore_blocked_not_running", {
state: supervisor.info.state,
}),
});
this._restoringBackup = false;
return;
}
if (
!(await showConfirmationDialog(this, {
title: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
}`
),
text: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
}`
),
confirmText: supervisor?.localize("backup.restore"),
dismissText: supervisor?.localize("backup.cancel"),
}))
) {
this._restoringBackup = false;
return;
}
try {
await restoreBackup(
this.hass,
this._backup!.type,
this._backup!.slug,
{ ...backupDetails, background: this._dialogParams?.onboarding },
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
);
this._dialogParams?.onRestoring?.();
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message ||
supervisor?.localize("backup.restore_start_failed");
} finally {
this._restoringBackup = false;
}
}
private async _deleteClicked() {
const supervisor = this._dialogParams?.supervisor;
if (!supervisor) return;
if (
!(await showConfirmationDialog(this, {
title: supervisor!.localize("backup.confirm_delete_title"),
text: supervisor!.localize("backup.confirm_delete_text"),
confirmText: supervisor!.localize("backup.delete"),
dismissText: supervisor!.localize("backup.cancel"),
destructive: true,
}))
) {
return;
}
try {
await removeBackup(this.hass!, this._backup!.slug);
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
this.closeDialog();
} catch (err: any) {
this._error = err.body.message;
}
}
private async _downloadClicked() {
const supervisor = this._dialogParams?.supervisor;
if (!supervisor) return;
let signedPath: { path: string };
try {
signedPath = await getSignedPath(
this.hass!,
`/api/hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/download`
);
} catch (err: any) {
await showAlertDialog(this, {
text: extractApiErrorMessage(err),
});
return;
}
if (window.location.href.includes("ui.nabu.casa")) {
const confirm = await showConfirmationDialog(this, {
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
dismissText: supervisor?.localize("backup.cancel"),
});
if (!confirm) {
return;
}
}
fileDownload(
signedPath.path,
`home_assistant_backup_${slugify(this._computeName)}.tar`
);
}
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
ha-icon-button {
color: var(--secondary-text-color);
}
.loading {
width: 100%;
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-backup": HassioBackupDialog;
}
}

View File

@@ -0,0 +1,158 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-spinner";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import {
createHassioFullBackup,
createHassioPartialBackup,
} from "../../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup";
@customElement("dialog-hassio-create-backup")
class HassioCreateBackupDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioCreateBackupDialogParams;
@state() private _error?: string;
@state() private _creatingBackup = false;
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
this._dialogParams = dialogParams;
this._creatingBackup = false;
}
public closeDialog() {
this._dialogParams = undefined;
this._creatingBackup = false;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this._dialogParams.supervisor.localize("backup.create_backup")
)}
>
${this._creatingBackup
? html`<ha-spinner></ha-spinner>`
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
dialogInitialFocus
>
</supervisor-backup-content>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this._dialogParams.supervisor.localize("common.close")}
</ha-button>
<ha-button
.disabled=${this._creatingBackup}
slot="primaryAction"
@click=${this._createBackup}
>
${this._dialogParams.supervisor.localize("backup.create")}
</ha-button>
</ha-dialog>
`;
}
private async _createBackup(): Promise<void> {
if (this._dialogParams!.supervisor.info.state !== "running") {
showAlertDialog(this, {
title: this._dialogParams!.supervisor.localize(
"backup.could_not_create"
),
text: this._dialogParams!.supervisor.localize(
"backup.create_blocked_not_running",
{ state: this._dialogParams!.supervisor.info.state }
),
});
return;
}
const backupDetails = this._backupContent.backupDetails();
this._creatingBackup = true;
this._error = "";
if (backupDetails.password && !backupDetails.password.length) {
this._error = this._dialogParams!.supervisor.localize(
"backup.enter_password"
);
this._creatingBackup = false;
return;
}
if (
backupDetails.password &&
backupDetails.password !== backupDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"backup.passwords_not_matching"
);
this._creatingBackup = false;
return;
}
delete backupDetails.confirm_password;
try {
if (this._backupContent.backupType === "full") {
await createHassioFullBackup(this.hass, backupDetails);
} else {
await createHassioPartialBackup(this.hass, backupDetails);
}
this._dialogParams!.onCreate();
this.closeDialog();
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
this._creatingBackup = false;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
:host {
direction: var(--direction);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-create-backup": HassioCreateBackupDialog;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-backup-upload";
export interface HassioBackupUploadDialogParams {
showBackup: (slug: string) => void;
reloadBackup?: () => Promise<void>;
onboarding?: boolean;
}
export const showBackupUploadDialog = (
element: HTMLElement,
dialogParams: HassioBackupUploadDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup-upload",
dialogImport: () => import("./dialog-hassio-backup-upload"),
dialogParams,
});
};

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupLocationDialogParams {
supervisor: Supervisor;
}
export const showHassioBackupLocationDialog = (
element: HTMLElement,
dialogParams: HassioBackupLocationDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup-location",
dialogImport: () => import("./dialog-hassio-backup-location"),
dialogParams,
});
};

View File

@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
slug: string;
onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
}
export const showHassioBackupDialog = (
element: HTMLElement,
dialogParams: HassioBackupDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-backup",
dialogImport: () => import("./dialog-hassio-backup"),
dialogParams,
});
};

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioCreateBackupDialogParams {
supervisor: Supervisor;
onCreate: () => void;
}
export const showHassioCreateBackupDialog = (
element: HTMLElement,
dialogParams: HassioCreateBackupDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-create-backup",
dialogImport: () => import("./dialog-hassio-create-backup"),
dialogParams,
});
};

View File

@@ -0,0 +1,184 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-select";
import "../../../../src/components/ha-spinner";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import type { DatadiskList } from "../../../../src/data/hassio/host";
import { listDatadisks, moveDatadisk } from "../../../../src/data/hassio/host";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk";
const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => {
// Assume a speed of 30 MB/s.
const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30;
const rebootTime = (supervisor.host.startup_time * 4) / 60;
return Math.ceil((moveTime + rebootTime) / 10) * 10;
});
@customElement("dialog-hassio-datadisk")
class HassioDatadiskDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private dialogParams?: HassioDatatiskDialogParams;
@state() private selectedDevice?: string;
@state() private devices?: DatadiskList["devices"];
@state() private moving = false;
public showDialog(params: HassioDatatiskDialogParams) {
this.dialogParams = params;
listDatadisks(this.hass).then((data) => {
this.devices = data.devices;
});
}
public closeDialog(): void {
this.dialogParams = undefined;
this.selectedDevice = undefined;
this.devices = undefined;
this.moving = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this.dialogParams) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this.moving
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
@closed=${this.closeDialog}
?hideActions=${this.moving}
>
${this.moving
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
<p class="progress-text">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving_desc"
)}
</p>`
: html` ${this.devices?.length
? html`
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.description",
{
current_path: this.dialogParams.supervisor.os.data_disk,
time: calculateMoveTime(this.dialogParams.supervisor),
}
)}
<br /><br />
<ha-select
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@selected=${this._selectDevice}
dialogInitialFocus
>
${this.devices.map(
(device) =>
html`<ha-list-item .value=${device}
>${device}</ha-list-item
>`
)}
</ha-select>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
"dialog.datadisk_move.loading_devices"
)
: this.dialogParams.supervisor.localize(
"dialog.datadisk_move.no_devices"
)}
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
</ha-button>
<ha-button
.disabled=${!this.selectedDevice}
slot="primaryAction"
@click=${this._moveDatadisk}
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.move"
)}
</ha-button>`}
</ha-dialog>
`;
}
private _selectDevice(ev) {
this.selectedDevice = ev.target.value;
}
private async _moveDatadisk() {
this.moving = true;
try {
await moveDatadisk(this.hass, this.selectedDevice!);
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.dialogParams!.supervisor.localize(
"system.host.failed_to_move"
),
text: extractApiErrorMessage(err),
});
this.closeDialog();
}
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-select {
width: 100%;
}
ha-spinner {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-datadisk": HassioDatadiskDialog;
}
}

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioDatatiskDialogParams {
supervisor: Supervisor;
}
export const showHassioDatadiskDialog = (
element: HTMLElement,
dialogParams: HassioDatatiskDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-datadisk",
dialogImport: () => import("./dialog-hassio-datadisk"),
dialogParams,
});
};

View File

@@ -0,0 +1,199 @@
import { mdiClose } from "@mdi/js";
import { dump } from "js-yaml";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/search-input";
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(hardware: HassioHardwareInfo, filter: string, language: string) =>
hardware.devices
.filter(
(device) =>
device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(dialogParams: HassioHardwareDialogParams) {
this._dialogParams = dialogParams;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._dialogParams) {
return nothing;
}
const devices = _filterDevices(
this._dialogParams.hardware,
(this._filter || "").toLowerCase(),
this.hass.locale.language
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${this._dialogParams.supervisor.localize(
"dialog.hardware.title"
)}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize("common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-icon-button {
position: absolute;
right: 16px;
inset-inline-end: 16px;
inset-inline-start: initial;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
margin-inline-start: 18px;
margin-inline-end: 42px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: var(--ha-border-radius-sm);
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--ha-font-family-code);
}
code {
font-size: var(--ha-font-size-s);
padding: 0.2em 0.4em;
}
search-input {
margin: 8px 16px 0;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};

View File

@@ -0,0 +1,70 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-markdown";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public content!: string;
@state() private _opened = false;
public showDialog(params: HassioMarkdownDialogParams) {
this.title = params.title;
this.content = params.content;
this._opened = true;
}
public closeDialog() {
this._opened = false;
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)}
hideactions
>
<ha-markdown
.content=${this.content || ""}
dialogInitialFocus
></ha-markdown>
</ha-dialog>
`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
hassioStyle,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-markdown {
padding: 16px;
}
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-markdown": HassioMarkdownDialog;
}
}

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
export interface HassioMarkdownDialogParams {
title: string;
content: string;
}
export const showHassioMarkdownDialog = (
element: HTMLElement,
dialogParams: HassioMarkdownDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown",
dialogImport: () => import("./dialog-hassio-markdown"),
dialogParams,
});
};

View File

@@ -0,0 +1,647 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list";
import "../../../../src/components/ha-list-item";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-tab-group";
import "../../../../src/components/ha-tab-group-tab";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import type {
AccessPoints,
NetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import {
accesspointScan,
updateNetworkInterface,
} from "../../../../src/data/hassio/network";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioNetworkDialogParams } from "./show-dialog-network";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network")
export class DialogHassioNetwork
extends LitElement
implements HassDialog<HassioNetworkDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _accessPoints?: AccessPoints;
@state() private _curTabIndex = 0;
@state() private _dirty = false;
@state() private _interface?: NetworkInterface;
@state() private _interfaces!: NetworkInterface[];
@state() private _params?: HassioNetworkDialogParams;
@state() private _processing = false;
@state() private _scanning = false;
@state() private _wifiConfiguration?: WifiConfiguration;
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this.supervisor = params.supervisor;
this._interfaces = params.supervisor.network.interfaces.sort((a, b) =>
a.primary > b.primary ? -1 : 1
);
this._interface = { ...this._interfaces[this._curTabIndex] };
await this.updateComplete;
}
public closeDialog() {
this._params = undefined;
this._processing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._params || !this._interface) {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${this.supervisor.localize("dialog.network.title")}
hideActions
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">
${this.supervisor.localize("dialog.network.title")}
</span>
<ha-icon-button
.label=${this.supervisor.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar>
${this._interfaces.length > 1
? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated}
>${this._interfaces.map(
(device, index) =>
html`<ha-tab-group-tab
slot="nav"
.id=${device.interface}
.panel=${index.toString()}
.active=${this._curTabIndex === index}
>
${device.interface}
</ha-tab-group-tab>`
)}
</ha-tab-group>`
: ""}
</div>
${cache(this._renderTab())}
</ha-dialog>
`;
}
private _renderTab() {
return html` <div class="form container">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel
.header=${this.supervisor.localize("dialog.network.wifi")}
outlined
>
${this._interface?.wifi?.ssid
? html`<p>
${this.supervisor.localize(
"dialog.network.connected_to",
{ ssid: this._interface?.wifi?.ssid }
)}
</p>`
: ""}
<ha-button
appearance="plain"
size="small"
class="scan"
@click=${this._scanForAP}
.disabled=${this._scanning}
.loading=${this._scanning}
>
${this.supervisor.localize("dialog.network.scan_ap")}
</ha-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<ha-list>
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) => html`
<ha-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} -
${this.supervisor.localize(
"dialog.network.signal_strength"
)}:
${ap.signal}
</span>
</ha-list-item>
`
)}
</ha-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.open"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wep"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wpa"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-password-field
class="flex-auto"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
)}
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-password-field>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<ha-alert alert-type="warning">
${this.supervisor.localize("dialog.network.warning")}
</ha-alert>`
: ""}
</div>
<div class="buttons">
<ha-button @click=${this.closeDialog} appearance="plain">
${this.supervisor.localize("common.cancel")}
</ha-button>
<ha-button
@click=${this._updateNetwork}
.disabled=${!this._dirty}
.loading=${this._processing}
>
${this.supervisor.localize("common.save")}
</ha-button>
</div>`;
}
private _selectAP(event) {
this._wifiConfiguration = event.currentTarget.ap;
this._dirty = true;
}
private async _scanForAP() {
if (!this._interface) {
return;
}
this._scanning = true;
try {
this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err: any) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.auto")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
dialogInitialFocus
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.static")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor.localize("dialog.network.disabled")}
class="warning"
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<ha-textfield
class="flex-auto"
id="address"
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
.version=${version}
.value=${this._toString(this._interface![version].address)}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
<ha-textfield
class="flex-auto"
id="gateway"
.label=${this.supervisor.localize("dialog.network.gateway")}
.version=${version}
.value=${this._interface![version].gateway}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
<ha-textfield
class="flex-auto"
id="nameservers"
.label=${this.supervisor.localize("dialog.network.dns_servers")}
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
`
: ""}
</ha-expansion-panel>
`;
}
private _toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
private _toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
}
private async _updateNetwork() {
this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
};
if (this._interface![version]?.method === "static") {
interfaceOptions[version] = {
...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address),
gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers),
};
}
});
if (this._wifiConfiguration) {
interfaceOptions = {
...interfaceOptions,
wifi: {
ssid: this._wifiConfiguration.ssid,
mode: this._wifiConfiguration.mode,
auth: this._wifiConfiguration.auth || "open",
},
};
if (interfaceOptions.wifi!.auth !== "open") {
interfaceOptions.wifi = {
...interfaceOptions.wifi,
psk: this._wifiConfiguration.psk,
};
}
}
interfaceOptions.enabled =
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.network.failed_to_change"),
text: extractApiErrorMessage(err),
});
this._processing = false;
return;
}
this._params?.loadData();
this.closeDialog();
}
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text: this.supervisor.localize("dialog.network.unsaved"),
confirmText: this.supervisor.localize("common.yes"),
dismissText: this.supervisor.localize("common.no"),
});
if (!confirm) {
this.requestUpdate("_interface");
return;
}
}
this._curTabIndex = Number(ev.detail.name);
this._interface = { ...this._interfaces[this._curTabIndex] };
}
private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as any).value as "disabled" | "auto" | "static";
const version = (ev.target as any).version as "ipv4" | "ipv6";
if (
!value ||
!this._interface ||
this._interface[version]!.method === value
) {
return;
}
this._dirty = true;
this._interface[version]!.method = value;
this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = (ev.target as any).value as string as
| "open"
| "wep"
| "wpa-psk";
this._wifiConfiguration!.auth = value;
this._dirty = true;
this.requestUpdate("_wifiConfiguration");
}
private _handleInputValueChanged(ev: Event): void {
const source = ev.target as HaTextField;
const value = source.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = source.id;
if (
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
return;
}
this._dirty = true;
this._interface[version]![id] = value;
}
private _handleInputValueChangedWifi(ev: Event): void {
const source = ev.target as HaTextField;
const value = source.value;
const id = source.id;
if (
!value ||
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
return;
}
this._dirty = true;
this._wifiConfiguration![id] = value;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
}
ha-dialog {
--dialog-content-position: static;
--dialog-content-padding: 0;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {
.container {
width: 400px;
}
}
.content {
display: block;
padding: 20px 24px;
}
/* overrule the ha-style-dialog max-height on small screens */
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-header-bar {
--mdc-theme-primary: var(--app-header-background-color);
--mdc-theme-on-primary: var(--app-header-text-color, white);
}
}
ha-button.scan {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.container {
padding: 0 8px 4px;
}
.form {
margin-bottom: 53px;
}
.buttons {
position: absolute;
bottom: 0;
width: 100%;
box-sizing: border-box;
border-top: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
display: flex;
justify-content: space-between;
padding: 16px;
padding-bottom: max(var(--safe-area-inset-bottom), 16px);
background-color: var(--mdc-theme-surface, #fff);
}
.warning {
color: var(--error-color);
--primary-color: var(--error-color);
}
div.warning {
margin: 12px 4px -12px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
ha-textfield {
padding: 0 14px;
}
ha-list-item {
--mdc-list-side-padding: 10px;
}
ha-tab-group-tab {
flex: 1;
}
ha-tab-group-tab::part(base) {
width: 100%;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-network": DialogHassioNetwork;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
supervisor: Supervisor;
loadData: () => Promise<void>;
}
export const showNetworkDialog = (
element: HTMLElement,
dialogParams: HassioNetworkDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network",
dialogImport: () => import("./dialog-hassio-network"),
dialogParams,
});
};

View File

@@ -2,22 +2,24 @@ import { mdiDelete, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-button";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../../data/hassio/docker";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
} from "../../../../src/data/hassio/docker";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
{
@@ -37,10 +39,12 @@ const SCHEMA = [
},
] as const;
@customElement("dialog-apps-registries")
class AppsRegistriesDialog extends LitElement {
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _registries?: {
registry: string;
username: string;
@@ -67,12 +71,8 @@ class AppsRegistriesDialog extends LitElement {
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? this.hass.localize(
"ui.panel.config.apps.dialog.registries.title_add"
)
: this.hass.localize(
"ui.panel.config.apps.dialog.registries.title_manage"
)
? this.supervisor.localize("dialog.registries.title_add")
: this.supervisor.localize("dialog.registries.title_manage")
)}
>
${this._addingRegistry
@@ -96,9 +96,7 @@ class AppsRegistriesDialog extends LitElement {
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dialog.registries.add_registry"
)}
${this.supervisor.localize("dialog.registries.add_registry")}
</ha-button>
</div>
`
@@ -108,15 +106,15 @@ class AppsRegistriesDialog extends LitElement {
<ha-settings-row class="registry">
<span slot="heading"> ${entry.registry} </span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.apps.dialog.registries.username"
${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}
</span>
<ha-icon-button
.entry=${entry}
.label=${this.hass.localize(
"ui.panel.config.apps.dialog.registries.remove"
.label=${this.supervisor.localize(
"dialog.registries.remove"
)}
.path=${mdiDelete}
@click=${this._removeRegistry}
@@ -126,8 +124,8 @@ class AppsRegistriesDialog extends LitElement {
)
: html`
<ha-alert>
${this.hass.localize(
"ui.panel.config.apps.dialog.registries.no_registries"
${this.supervisor.localize(
"dialog.registries.no_registries"
)}
</ha-alert>
`}
@@ -139,8 +137,8 @@ class AppsRegistriesDialog extends LitElement {
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dialog.registries.add_new_registry"
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
</ha-button>
</div> `}
@@ -149,17 +147,16 @@ class AppsRegistriesDialog extends LitElement {
}
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass.localize(
`ui.panel.config.apps.dialog.registries.${schema.name}` as any
);
this.supervisor.localize(`dialog.registries.${schema.name}`);
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;
}
public async showDialog(): Promise<void> {
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true;
this._input = {};
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
}
@@ -204,9 +201,7 @@ class AppsRegistriesDialog extends LitElement {
this._input = {};
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dialog.registries.failed_to_add"
),
title: this.supervisor.localize("dialog.registries.failed_to_add"),
text: extractApiErrorMessage(err),
});
}
@@ -220,9 +215,7 @@ class AppsRegistriesDialog extends LitElement {
await this._loadRegistries();
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.apps.dialog.registries.failed_to_remove"
),
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
text: extractApiErrorMessage(err),
});
}
@@ -257,6 +250,6 @@ class AppsRegistriesDialog extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-apps-registries": AppsRegistriesDialog;
"dialog-hassio-registries": HassioRegistriesDialog;
}
}

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-registries";
export interface RegistriesDialogParams {
supervisor: Supervisor;
}
export const showRegistriesDialog = (
element: HTMLElement,
dialogParams: RegistriesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogParams,
});
};

View File

@@ -3,44 +3,41 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-textfield";
import type { HaTextField } from "../../../../../components/ha-textfield";
import "../../../../../components/ha-tooltip";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-tooltip";
import type {
HassioAddonInfo,
HassioAddonsInfo,
HassioAddonRepository,
} from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addStoreRepository,
fetchStoreRepositories,
removeStoreRepository,
} from "../../../../../data/supervisor/store";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { RepositoryDialogParams } from "./show-dialog-repositories";
} from "../../../../src/data/supervisor/store";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@customElement("dialog-apps-repositories")
class AppsRepositoriesDialog extends LitElement {
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@query("#repository_input", true) private _optionInput?: HaTextField;
@state() private _repositories?: HassioAddonRepository[];
@state() private _dialogParams?: RepositoryDialogParams;
@state() private _addon?: HassioAddonsInfo;
@state() private _dialogParams?: HassioRepositoryDialogParams;
@state() private _opened = false;
@@ -48,16 +45,16 @@ class AppsRepositoriesDialog extends LitElement {
@state() private _error?: string;
public async showDialog(dialogParams: RepositoryDialogParams): Promise<void> {
public async showDialog(
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this._addon = dialogParams.addon;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams?.closeCallback?.();
this._dialogParams = undefined;
this._opened = false;
this._error = "";
@@ -67,9 +64,9 @@ class AppsRepositoriesDialog extends LitElement {
repos
.filter(
(repo) =>
repo.slug !== "core" && // The core apps repository
repo.slug !== "local" && // Locally managed apps
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
repo.slug !== "core" && // The core add-ons repository
repo.slug !== "local" && // Locally managed add-ons
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
repo.slug !== "5c53de3b" && // The ESPHome repository
repo.slug !== "d5369777" // Music Assistant repository
)
@@ -88,13 +85,13 @@ class AppsRepositoriesDialog extends LitElement {
);
protected render() {
if (!this._addon || this._repositories === undefined) {
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return nothing;
}
const repositories = this._filteredRepositories(this._repositories);
const usedRepositories = this._filteredUsedRepositories(
repositories,
this._addon.addons
this._dialogParams.supervisor.addon.addons
);
return html`
<ha-dialog
@@ -104,7 +101,7 @@ class AppsRepositoriesDialog extends LitElement {
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.apps.dialog.repositories.title")
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
>
${this._error
@@ -126,10 +123,10 @@ class AppsRepositoriesDialog extends LitElement {
class="delete"
slot="end"
>
${this.hass.localize(
${this._dialogParams!.supervisor.localize(
usedRepositories.includes(repo.slug)
? "ui.panel.config.apps.dialog.repositories.used"
: "ui.panel.config.apps.dialog.repositories.remove"
? "dialog.repositories.used"
: "dialog.repositories.remove"
)}
</ha-tooltip>
<div .id="icon-button-${repo.slug}">
@@ -147,8 +144,8 @@ class AppsRepositoriesDialog extends LitElement {
`
)
: html`<ha-md-list-item
>${this.hass.localize(
"ui.panel.config.apps.dialog.repositories.no_repositories"
>${this._dialogParams!.supervisor.localize(
"dialog.repositories.no_repositories"
)}</ha-md-list-item
>`}
</ha-md-list>
@@ -156,9 +153,9 @@ class AppsRepositoriesDialog extends LitElement {
<ha-textfield
class="flex-auto"
id="repository_input"
.value=${this._dialogParams?.url || ""}
.label=${this.hass.localize(
"ui.panel.config.apps.dialog.repositories.add"
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
dialogInitialFocus
@@ -170,14 +167,14 @@ class AppsRepositoriesDialog extends LitElement {
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.apps.dialog.repositories.add"
${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</ha-button>
</div>
</div>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
${this._dialogParams?.supervisor.localize("common.close")}
</ha-button>
</ha-dialog>
`;
@@ -200,8 +197,8 @@ class AppsRepositoriesDialog extends LitElement {
margin-top: 4px;
}
ha-button {
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
div.delete ha-icon-button {
@@ -235,7 +232,7 @@ class AppsRepositoriesDialog extends LitElement {
try {
this._repositories = await fetchStoreRepositories(this.hass);
fireEvent(this, "apps-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
@@ -252,7 +249,7 @@ class AppsRepositoriesDialog extends LitElement {
await addStoreRepository(this.hass, input.value);
await this._loadData();
fireEvent(this, "apps-collection-refresh", { collection: "store" });
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
input.value = "";
} catch (err: any) {
@@ -267,7 +264,7 @@ class AppsRepositoriesDialog extends LitElement {
await removeStoreRepository(this.hass, slug);
await this._loadData();
fireEvent(this, "apps-collection-refresh", { collection: "store" });
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
@@ -276,6 +273,6 @@ class AppsRepositoriesDialog extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-apps-repositories": AppsRepositoriesDialog;
"dialog-hassio-repositories": HassioRepositoriesDialog;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
supervisor: Supervisor;
url?: string;
}
export const showRepositoriesDialog = (
element: HTMLElement,
dialogParams: HassioRepositoryDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-repositories",
dialogImport: () => import("./dialog-hassio-repositories"),
dialogParams,
});
};

View File

@@ -0,0 +1,38 @@
import type { LitElement } from "lit";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { restartHassioAddon } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../src/types";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
supervisor: Supervisor,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: supervisor.localize("dialog.restart_addon.title", {
name: addon.name,
}),
text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.restart"),
dismissText: supervisor.localize("common.cancel"),
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err: any) {
showAlertDialog(element, {
title: supervisor.localize("common.failed_to_restart_name", {
name: addon.name,
}),
text: extractApiErrorMessage(err),
});
}
}
};

View File

@@ -0,0 +1,192 @@
import { mdiClose, mdiPuzzle, mdiSwapHorizontal } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { atLeastVersion } from "../../../../src/common/config/version";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-icon-next";
import "../../../../src/components/ha-md-dialog";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import {
getConfigEntry,
type ConfigEntry,
} from "../../../../src/data/config_entries";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { brandsUrl } from "../../../../src/util/brands-url";
import type { SystemManagedDialogParams } from "./show-dialog-system-managed";
@customElement("dialog-system-managed")
class HassioSystemManagedDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _supervisor?: Supervisor;
@state() private _addon?: HassioAddonDetails;
@state() private _open = false;
@state() private _configEntry?: ConfigEntry;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: SystemManagedDialogParams
): Promise<void> {
this._addon = dialogParams.addon;
this._supervisor = dialogParams.supervisor;
this._open = true;
this._loadConfigEntry();
}
private _dialogClosed() {
this._addon = undefined;
this._supervisor = undefined;
this._configEntry = undefined;
this._open = false;
}
public closeDialog() {
this._dialog?.close();
return true;
}
protected render() {
if (!this._addon || !this._open || !this._supervisor) {
return nothing;
}
const addonImage =
atLeastVersion(this.hass.config.version, 0, 105) && this._addon.icon
? `/api/hassio/addons/${this._addon.slug}/icon`
: undefined;
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title">${this._addon?.name}</span>
</ha-dialog-header>
<div slot="content">
<div class="icons">
<ha-svg-icon
class="primary"
.path=${mdiHomeAssistant}
></ha-svg-icon>
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
${addonImage
? html`<img src=${addonImage} alt=${this._addon.name} />`
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
</div>
${this._supervisor.localize("addon.system_managed.title")}.<br />
${this._supervisor.localize("addon.system_managed.description")}
${this._configEntry
? html`
<h3>
${this._supervisor.localize(
"addon.system_managed.managed_by"
)}:
</h3>
<ha-md-list>
<ha-md-list-item
type="link"
href=${`/config/integrations/integration/${this._configEntry.domain}`}
>
<img
slot="start"
class="integration-icon"
alt=${this._configEntry.title}
src=${brandsUrl({
domain: this._configEntry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
${this._configEntry.title}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
`
: nothing}
</div>
</ha-md-dialog>
`;
}
private _onImageLoad(ev) {
ev.target.style.visibility = "initial";
}
private _onImageError(ev) {
ev.target.style.visibility = "hidden";
}
private async _loadConfigEntry() {
if (this._addon?.system_managed_config_entry) {
try {
const { config_entry } = await getConfigEntry(
this.hass,
this._addon.system_managed_config_entry
);
this._configEntry = config_entry;
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.icons {
display: flex;
justify-content: center;
align-items: center;
gap: var(--ha-space-4);
--mdc-icon-size: 48px;
margin-bottom: 32px;
}
.icons img {
width: 48px;
}
.icons .primary {
color: var(--primary-color);
}
.actions {
display: flex;
justify-content: space-between;
}
.integration-icon {
width: 24px;
}
ha-md-list-item {
--md-list-item-leading-space: 4px;
--md-list-item-trailing-space: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-system-managed": HassioSystemManagedDialog;
}
}

View File

@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SystemManagedDialogParams {
addon: HassioAddonDetails;
supervisor: Supervisor;
}
export const showSystemManagedDialog = (
element: HTMLElement,
dialogParams: SystemManagedDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-system-managed",
dialogImport: () => import("./dialog-system-managed"),
dialogParams,
});
};

View File

@@ -0,0 +1,23 @@
(function () {
function loadES5(src) {
var el = document.createElement("script");
el.src = src;
document.body.appendChild(el);
}
if (<%= modernRegex %>.test(navigator.userAgent)) {
try {
<% for (const entry of latestEntryJS) { %>
new Function("import('<%= entry %>')")();
<% } %>
} catch (err) {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
} else {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
})();

28
hassio/src/entrypoint.ts Normal file
View File

@@ -0,0 +1,28 @@
import {
haFontFamilyBody,
haFontSmoothing,
haMozOsxFontSmoothing,
} from "../../src/resources/theme/typography.globals";
import "./hassio-main";
import("../../src/resources/append-ha-style");
const styleEl = document.createElement("style");
styleEl.textContent = `
body {
font-family: ${haFontFamilyBody};
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
-webkit-font-smoothing: ${haFontSmoothing};
font-weight: var(--ha-font-weight-normal);
margin: 0;
padding: 0;
height: 100vh;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
}
}
`;
document.head.appendChild(styleEl);

146
hassio/src/hassio-main.ts Normal file
View File

@@ -0,0 +1,146 @@
import type { PropertyValues } from "lit";
import { html } from "lit";
import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { mainWindow } from "../../src/common/dom/get_main_window";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../src/types";
import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element";
@customElement("hassio-main")
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow = false;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyTheme();
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
// So here we are going to translate toggle events into the appropriate
// open and close events. These events are a no-op in newer versions of
// Home Assistant.
this.addEventListener("hass-toggle-menu", () => {
fireEvent(
(window.parent as any).customPanel,
// @ts-ignore
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
);
});
// Paulus - March 19, 2019
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
// Joakim - April 26, 2021
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
mainWindow.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
})
);
// Paulus - May 17, 2021
// Convert the <a> tags to native nav in Home Assistant < 2021.6
document.body.addEventListener("click", (ev) => {
const href = isNavigationClick(ev);
if (href) {
navigate(href);
}
});
// Forward haptic events to parent window.
window.addEventListener("haptic", (ev) => {
// @ts-ignore
fireEvent(window.parent, ev.type, ev.detail, {
bubbles: false,
});
});
// Forward keydown events to the main window for quickbar access
document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
// Ignore if modifier keys are pressed
return;
}
// @ts-ignore
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
bubbles: false,
});
});
makeDialogManager(this, this.shadowRoot!);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass) {
return;
}
if (oldHass.themes !== this.hass.themes) {
this._applyTheme();
}
}
protected render() {
return html`
<hassio-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.panel=${this.panel}
.narrow=${this.narrow}
></hassio-router>
`;
}
private _applyTheme() {
let themeName: string;
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
if (atLeastVersion(this.hass.config.version, 0, 114)) {
themeName =
this.hass.selectedTheme?.theme ||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
? this.hass.themes.default_dark_theme!
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
} else {
themeName =
(this.hass.selectedTheme as unknown as string) ||
this.hass.themes.default_theme;
}
applyThemesOnElement(
this.parentElement,
this.hass.themes,
themeName,
themeSettings,
true
);
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-main": HassioMain;
}
}

View File

@@ -0,0 +1,155 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
import type { TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { navigate } from "../../src/common/navigate";
import {
createSearchParam,
extractSearchParamsObject,
} from "../../src/common/url/search-params";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-error-screen";
import type {
ParamType,
Redirect,
Redirects,
} from "../../src/panels/my/ha-panel-my";
import type { HomeAssistant, Route } from "../../src/types";
export const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
supervisor_info: {
redirect: "/hassio/system",
},
supervisor_snapshots: {
redirect: "/hassio/backups",
},
supervisor_backups: {
redirect: "/hassio/backups",
},
supervisor_store: {
redirect: "/hassio/store",
},
supervisor_addons: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
optional_params: {
repository_url: "url",
},
},
supervisor_ingress: {
redirect: "/hassio/ingress",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")
class HassioMyRedirect extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@state() public _error?: TemplateResult | string;
connectedCallback() {
super.connectedCallback();
const path = this.route.path.substr(1);
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = this.supervisor.localize("my.not_supported", {
link: html`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>
${this.supervisor.localize("my.faq_link")}
</a>`,
});
return;
}
let url: string;
try {
url = this._createRedirectUrl(redirect);
} catch (_err: any) {
this._error = this.supervisor.localize("my.error");
return;
}
navigate(url, { replace: true });
}
protected render() {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
return nothing;
}
private _createRedirectUrl(redirect: Redirect): string {
const params = this._createRedirectParams(redirect);
return `${redirect.redirect}${params}`;
}
private _createRedirectParams(redirect: Redirect): string {
const params = extractSearchParamsObject();
if (!redirect.params && !Object.keys(params).length) {
return "";
}
const resultParams = {};
Object.entries(redirect.params || {}).forEach(([key, type]) => {
if (!params[key] || !this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
});
Object.entries(redirect.optional_params || {}).forEach(([key, type]) => {
if (params[key]) {
if (!this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
}
});
return `?${createSearchParam(resultParams)}`;
}
private _checkParamType(type: ParamType, value: string) {
if (type === "string") {
return true;
}
if (type === "url") {
return value && value === sanitizeUrl(value);
}
return false;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-my-redirect": HassioMyRedirect;
}
}

View File

@@ -0,0 +1,53 @@
import { customElement, property } from "lit/decorators";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../src/layouts/hass-router-page";
import type { HomeAssistant, Route } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./dashboard/hassio-dashboard";
@customElement("hassio-panel-router")
class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow = false;
protected routerOptions: RouterOptions = {
beforeRender: (page: string) =>
page === "snapshots" ? "backups" : undefined,
routes: {
dashboard: {
tag: "hassio-dashboard",
},
store: {
tag: "hassio-addon-store",
load: () => import("./addon-store/hassio-addon-store"),
},
backups: {
tag: "hassio-backups",
load: () => import("./backups/hassio-backups"),
},
system: {
tag: "hassio-system",
load: () => import("./system/hassio-system"),
},
},
};
protected updatePageEl(el) {
el.hass = this.hass;
el.supervisor = this.supervisor;
el.route = this.route;
el.narrow = this.narrow;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-panel-router": HassioPanelRouter;
}
}

View File

@@ -0,0 +1,55 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-loading-screen";
import type { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow}
></hassio-panel-router>
`;
}
static styles = css`
:host {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-panel": HassioPanel;
}
}

View File

@@ -0,0 +1,91 @@
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import type { Supervisor } from "../../src/data/supervisor/supervisor";
import type { RouterOptions } from "../../src/layouts/hass-router-page";
import { HassRouterPage } from "../../src/layouts/hass-router-page";
import type { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
@customElement("hassio-router")
class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow = false;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
beforeRender: (page: string) => {
if (page === "snapshots") {
return "backups";
}
if (page === "dashboard" && this.panel.config?.ingress) {
return "ingress";
}
return undefined;
},
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
backups: "dashboard",
store: "dashboard",
system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: {
tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"),
},
ingress: {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
_my_redirect: {
tag: "hassio-my-redirect",
load: () => import("./hassio-my-redirect"),
},
},
};
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const hassioPanel = el.localName === "hassio-panel";
const ingressPanel = el.localName === "hassio-ingress-view";
const route = hassioPanel
? this.route
: ingressPanel && this.panel.config?.ingress
? this._ingressRoute(this.panel.config?.ingress)
: this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
el.route = route;
el.supervisor = this.supervisor;
if (ingressPanel) {
el.ingressPanel = Boolean(this.panel.config?.ingress);
}
}
private _ingressRoute = memoizeOne((ingress: string) => ({
prefix: "/hassio/ingress",
path: `/${ingress}`,
}));
}
declare global {
interface HTMLElementTagNameMap {
"hassio-router": HassioRouter;
}
}

34
hassio/src/hassio-tabs.ts Normal file
View File

@@ -0,0 +1,34 @@
import {
mdiBackupRestore,
mdiCogs,
mdiPuzzle,
mdiViewDashboard,
} from "@mdi/js";
import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
atLeastVersion(hass.config.version, 2022, 5)
? []
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];

View File

@@ -0,0 +1,377 @@
import { mdiMenu } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { goBack, navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status";
import "../../../src/components/ha-icon-button";
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
import {
fetchHassioAddonInfo,
startHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
createHassioSession,
validateHassioSession,
} from "../../../src/data/hassio/ingress";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../src/types";
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public ingressPanel = false;
@property({ type: Boolean }) public narrow = false;
@state() private _addon?: HassioAddonDetails;
@state() private _loadingMessage?: string;
private _sessionKeepAlive?: number;
private _fetchDataTimeout?: number;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
}
protected render(): TemplateResult {
if (!this._addon) {
return html`<hass-loading-screen
.message=${this._loadingMessage}
></hass-loading-screen>`;
}
const iframe = html`<iframe
title=${this._addon.name}
src=${this._addon.ingress_url!}
@load=${this._checkLoaded}
>
</iframe>`;
if (!this.ingressPanel) {
return html`<hass-subpage
.hass=${this.hass}
.header=${this._addon.name}
.narrow=${this.narrow}
>
${iframe}
</hass-subpage>`;
}
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header">
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`
: iframe}`;
}
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon");
let addonInfo: HassioAddonDetails;
if (requestedAddon) {
try {
addonInfo = await fetchHassioAddonInfo(this.hass, requestedAddon);
} catch (err: any) {
await showAlertDialog(this, {
text: extractApiErrorMessage(err),
title: requestedAddon,
});
await nextRender();
navigate("/hassio/store", { replace: true });
return;
}
if (!addonInfo.version) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
}
}
}
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("route")) {
return;
}
const addon = this.route.path.substring(1);
const oldRoute = changedProps.get("route") as this["route"] | undefined;
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
if (addon && addon !== oldAddon) {
this._loadingMessage = undefined;
this._fetchData(addon);
}
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass);
let addon: HassioAddonDetails;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (_err: any) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_info") ||
"Unable to fetch add-on info to start Ingress",
title: "Supervisor",
});
await nextRender();
navigate("/hassio/store", { replace: true });
return;
}
if (!addon.version) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_installed") ||
"The add-on is not installed. Please install it first",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return;
}
if (!addon.ingress_url) {
await this.updateComplete;
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_supported") ||
"This add-on does not support Ingress",
title: addon.name,
});
await nextRender();
goBack();
return;
}
if (!addon.state || !["startup", "started"].includes(addon.state)) {
await this.updateComplete;
const confirm = await showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_running") ||
"The add-on is not running. Do you want to start it now?",
title: addon.name,
confirmText:
this.supervisor.localize("ingress.start_addon") || "Start add-on",
dismissText: this.supervisor.localize("common.no") || "No",
});
if (confirm) {
try {
this._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
await startHassioAddon(this.hass, addonSlug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
this._fetchData(addonSlug);
return;
} catch (_err) {
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_starting_addon") ||
"Error starting the add-on",
title: addon.name,
});
await nextRender();
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
return;
}
} else {
await nextRender();
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
return;
}
}
if (addon.state === "startup") {
// Addon is starting up, wait for it to start
this._loadingMessage =
this.supervisor.localize("ingress.addon_starting") ||
"The add-on is starting, this can take some time...";
this._fetchDataTimeout = window.setTimeout(() => {
this._fetchData(addonSlug);
}, 500);
return;
}
if (addon.state !== "started") {
return;
}
this._loadingMessage = undefined;
if (this._fetchDataTimeout) {
clearInterval(this._fetchDataTimeout);
this._fetchDataTimeout = undefined;
}
let session: string;
try {
session = await createSessionPromise;
} catch (_err: any) {
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
await showAlertDialog(this, {
text:
this.supervisor.localize("ingress.error_creating_session") ||
"Unable to create an Ingress session",
title: addon.name,
});
await nextRender();
goBack();
return;
}
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
this._sessionKeepAlive = window.setInterval(async () => {
try {
await validateHassioSession(this.hass, session);
} catch (_err: any) {
session = await createHassioSession(this.hass);
}
}, 60000);
this._addon = addon;
}
private async _checkLoaded(ev): Promise<void> {
if (!this._addon) {
return;
}
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
await this.updateComplete;
showConfirmationDialog(this, {
text:
this.supervisor.localize("ingress.error_addon_not_ready") ||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
title: this._addon.name,
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
dismissText: this.supervisor.localize("common.no") || "No",
confirm: async () => {
const addon = this._addon;
this._addon = undefined;
await Promise.all([
this.updateComplete,
new Promise((resolve) => {
setTimeout(resolve, 500);
}),
]);
this._addon = addon;
},
});
}
}
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
}
static styles = css`
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.header + iframe {
height: calc(100% - 40px);
}
.header {
display: flex;
align-items: center;
font-size: var(--ha-font-size-l);
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: var(--ha-font-weight-normal);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
--mdc-icon-size: 20px;
}
.main-title {
margin: var(--margin-title);
line-height: var(--ha-line-height-condensed);
flex-grow: 1;
}
ha-icon-button {
pointer-events: auto;
}
hass-subpage {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hassio-ingress-view": HassioIngressView;
}
}

View File

@@ -1,8 +1,8 @@
import { css } from "lit";
export const supervisorAppsStyle = css`
export const hassioStyle = css`
.content {
margin: var(--ha-space-2);
margin: 8px;
}
h1,
.description,
@@ -11,21 +11,21 @@ export const supervisorAppsStyle = css`
}
h1 {
font-size: 2em;
margin-bottom: var(--ha-space-2);
margin-bottom: 8px;
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
padding-left: var(--ha-space-2);
padding-inline-start: var(--ha-space-2);
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.description {
margin-top: var(--ha-space-1);
padding-left: var(--ha-space-2);
padding-inline-start: var(--ha-space-2);
margin-top: 4px;
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.card-group {
@@ -50,6 +50,6 @@ export const supervisorAppsStyle = css`
}
.error {
color: var(--error-color);
margin-top: var(--ha-space-4);
margin-top: 16px;
}
`;

View File

@@ -0,0 +1,231 @@
import type { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement } from "lit";
import { property, state } from "lit/decorators";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import type { HassioResponse } from "../../src/data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
} from "../../src/data/hassio/host";
import { fetchNetworkInfo } from "../../src/data/hassio/network";
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
import {
fetchHassioHomeAssistantInfo,
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import type {
Supervisor,
SupervisorObject,
SupervisorKeys,
} from "../../src/data/supervisor/supervisor";
import {
getSupervisorEventCollection,
supervisorCollection,
cleanupSupervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import type { HomeAssistant, Route } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
import {
computeRTLDirection,
setDirectionStyles,
} from "../../src/common/util/compute_rtl";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public route?: Route;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@state() private _unsubs: Record<string, UnsubscribeFunc> = {};
@state() private _collections: Record<string, Collection<unknown>> = {};
@state() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
if (!this.hasUpdated) {
return;
}
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
delete this._unsubs[unsub];
});
Object.keys(this._collections).forEach((collection) => {
cleanupSupervisorCollection(this.hass.connection, collection);
});
this._collections = {};
this.removeEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
}
protected willUpdate(changedProperties: PropertyValues) {
if (!this.hasUpdated) {
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (oldHass?.language !== this.hass.language) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language") || !this.hasUpdated) {
this._initializeLocalize();
this._applyDirection(this.hass);
}
}
protected _updateSupervisor(update: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor, ...update };
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(null, this._language);
this._updateSupervisor({
localize: await computeLocalize<SupervisorKeys>(
this.constructor.prototype,
language,
{
[language]: data,
}
),
});
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
if (collection in this._collections) {
this._collections[collection].refresh();
}
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [collection]: response.data });
}
private _subscribeCollection(collection: string) {
if (this._unsubs[collection]) {
this._unsubs[collection]();
}
try {
this._unsubs[collection] = this._collections[collection].subscribe(
(data) =>
this._updateSupervisor({
[collection]: data,
})
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._subscribeCollection(collection);
this._collections[collection].refresh();
} else {
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
collection,
supervisorCollection[collection]
);
if (this._collections[collection].state) {
// happens when the grace period of the collection unsubscribe has not passed yet
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
}
this._subscribeCollection(collection);
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this._updateSupervisor({
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
});
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
}
private _applyDirection(hass: HomeAssistant) {
const direction = computeRTLDirection(hass);
setDirectionStyles(direction, this);
}
}

View File

@@ -0,0 +1,206 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import type { HassioStats } from "../../../src/data/hassio/common";
import {
extractApiErrorMessage,
fetchHassioStats,
} from "../../../src/data/hassio/common";
import { restartCore } from "../../../src/data/supervisor/core";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
class HassioCoreInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@state() private _metrics?: HassioStats;
protected render(): TemplateResult | undefined {
const metrics = [
{
description: this.supervisor.localize("system.core.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: this.supervisor.localize("system.core.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
)}`,
},
];
return html`
<ha-card header="Core" outlined>
<div class="card-content">
<div>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
core-${this.supervisor.core.version}
</span>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
core-${this.supervisor.core.version_latest}
</span>
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.core.update_available
? html`
<ha-button
appearance="plain"
href="/hassio/update-available/core"
>
${this.supervisor.localize("common.show")}
</ha-button>
`
: ""}
</ha-settings-row>
</div>
<div>
${metrics.map(
(metric) => html`
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-metric>
`
)}
</div>
</div>
<div class="card-actions">
<ha-progress-button
slot="primaryAction"
variant="danger"
@click=${this._coreRestart}
.title=${this.supervisor.localize("common.restart_name", {
name: "Core",
})}
>
${this.supervisor.localize("common.restart_name", { name: "Core" })}
</ha-progress-button>
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
}
private async _loadData(): Promise<void> {
this._metrics = await fetchHassioStats(this.hass, "core");
}
private async _coreRestart(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.restart.title", {
name: "Home Assistant Core",
}),
text: this.supervisor.localize("confirm.restart.text", {
name: "Home Assistant Core",
}),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await restartCore(this.hass);
} catch (err: any) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize("common.failed_to_restart_name", {
name: "Home Assistant Core",
}),
text: extractApiErrorMessage(err),
});
}
} finally {
button.progress = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: flex-end;
align-items: center;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 124px);
justify-content: space-between;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-core-info": HassioCoreInfo;
}
}

View File

@@ -0,0 +1,453 @@
import { mdiDotsVertical } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-list-item";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
rebootHost,
shutdownHost,
} from "../../../src/data/hassio/host";
import type { NetworkInfo } from "../../../src/data/hassio/network";
import { fetchNetworkInfo } from "../../../src/data/hassio/network";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import type { HomeAssistant } from "../../../src/types";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../src/util/calculate";
import "../components/supervisor-metric";
import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
protected render(): TemplateResult | undefined {
const primaryIpAddress = this.supervisor.host.features.includes("network")
? this._primaryIpAddress(this.supervisor.network!)
: "";
const metrics = [
{
description: this.supervisor.localize("system.host.used_space"),
value: this._getUsedSpace(
this.supervisor.host.disk_used,
this.supervisor.host.disk_total
),
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
},
];
return html`
<ha-card header="Host" outlined>
<div class="card-content">
<div>
${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.hostname")}
</span>
<span slot="description">
${this.supervisor.host.hostname}
</span>
<ha-button
@click=${this._changeHostnameClicked}
appearance="plain"
size="small"
>
${this.supervisor.localize("system.host.change")}
</ha-button>
</ha-settings-row>`
: ""}
${this.supervisor.host.features.includes("network")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.ip_address")}
</span>
<span slot="description"> ${primaryIpAddress} </span>
<ha-button
@click=${this._changeNetworkClicked}
appearance="plain"
size="small"
>
${this.supervisor.localize("system.host.change")}
</ha-button>
</ha-settings-row>`
: ""}
<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.operating_system")}
</span>
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.os.update_available
? html`
<ha-button
appearance="plain"
size="small"
href="/hassio/update-available/os"
>
${this.supervisor.localize("common.show")}
</ha-button>
`
: ""}
</ha-settings-row>
${!this.supervisor.host.features.includes("haos")
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.docker_version")}
</span>
<span slot="description">
${this.supervisor.info.docker}
</span>
</ha-settings-row>`
: ""}
${this.supervisor.host.deployment
? html`<ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.deployment")}
</span>
<span slot="description">
${this.supervisor.host.deployment}
</span>
</ha-settings-row>`
: ""}
</div>
<div>
${this.supervisor.host.disk_life_time !== null
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.lifetime_used")}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`
: ""}
${metrics.map(
(metric) => html`
<supervisor-metric
.description=${metric.description}
.value=${metric.value ?? 0}
.tooltip=${metric.tooltip}
></supervisor-metric>
`
)}
</div>
</div>
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
? html`
<ha-progress-button variant="danger" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")}
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
? html`
<ha-progress-button
variant="danger"
@click=${this._hostShutdown}
>
${this.supervisor.localize("system.host.shutdown_host")}
</ha-progress-button>
`
: ""}
<ha-button-menu>
<ha-icon-button
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<ha-list-item
.action=${"hardware"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize("system.host.hardware")}
</ha-list-item>
${this.supervisor.host.features.includes("haos")
? html`
<ha-list-item
.action=${"import_from_usb"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize("system.host.import_from_usb")}
</ha-list-item>
${this.supervisor.host.features.includes("os_agent") &&
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
? html`
<ha-list-item
.action=${"move_datadisk"}
@click=${this._handleMenuAction}
>
${this.supervisor.localize(
"system.host.move_datadisk"
)}
</ha-list-item>
`
: ""}
`
: ""}
</ha-button-menu>
</div>
</ha-card>
`;
}
protected firstUpdated(): void {
this._loadData();
}
private _getUsedSpace = memoizeOne((used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total))
);
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info || !network_info.interfaces) {
return "";
}
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
});
private async _handleMenuAction(ev) {
switch ((ev.target as any).action) {
case "hardware":
await this._showHardware();
break;
case "import_from_usb":
await this._importFromUSB();
break;
case "move_datadisk":
await this._moveDatadisk();
break;
}
}
private _moveDatadisk(): void {
showHassioDatadiskDialog(this, {
supervisor: this.supervisor,
});
}
private async _showHardware(): Promise<void> {
let hardware;
try {
hardware = await fetchHassioHardwareInfo(this.hass);
} catch (err: any) {
await showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
text: extractApiErrorMessage(err),
});
return;
}
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.reboot_host"),
text: this.supervisor.localize("system.host.confirm_reboot"),
confirmText: this.supervisor.localize("system.host.reboot_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("system.host.shutdown_host"),
text: this.supervisor.localize("system.host.confirm_shutdown"),
confirmText: this.supervisor.localize("system.host.shutdown_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err: any) {
// Ignore connection errors, these are all expected
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
supervisor: this.supervisor,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, {
title: this.supervisor.localize("system.host.change_hostname"),
inputLabel: this.supervisor.localize("system.host.new_hostname"),
inputType: "string",
defaultValue: curHostname,
confirmText: this.supervisor.localize("common.update"),
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
text: extractApiErrorMessage(err),
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_import_from_usb"
),
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
ha-card {
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
.card-actions {
height: 48px;
border-top: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
display: flex;
flex-direction: column;
height: calc(100% - 124px);
justify-content: space-between;
}
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row[three-line] {
height: 74px;
}
ha-settings-row > span[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
}
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
ha-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
a {
text-decoration: none;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-host-info": HassioHostInfo;
}
}

Some files were not shown because too many files have changed in this diff Show More