mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-26 01:17:55 +00:00
Compare commits
37 Commits
animate-ha
...
fix-toolti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6d1216da | ||
|
|
5b5c671d89 | ||
|
|
e6462835e5 | ||
|
|
fa08a9801e | ||
|
|
c1d7100e91 | ||
|
|
0f1ffaf5ac | ||
|
|
5ca8fd4095 | ||
|
|
c3277ff8b2 | ||
|
|
aac463b34d | ||
|
|
c8471cb623 | ||
|
|
bd33a94749 | ||
|
|
6061f72f3a | ||
|
|
de85b08de4 | ||
|
|
2599804d22 | ||
|
|
27e1fc9b91 | ||
|
|
ae627a9c66 | ||
|
|
12623c31da | ||
|
|
1ddbf4ba09 | ||
|
|
af4d68e2b6 | ||
|
|
45b28d382c | ||
|
|
be007399cc | ||
|
|
823b4fc4f6 | ||
|
|
4e4882b9fa | ||
|
|
55c74d7959 | ||
|
|
3231d46835 | ||
|
|
0d110cfc7e | ||
|
|
936c0cd6aa | ||
|
|
311cbad8fa | ||
|
|
73a1ce90c3 | ||
|
|
ea1b7b9dec | ||
|
|
fd506d4d72 | ||
|
|
a3be09018c | ||
|
|
3364d4f578 | ||
|
|
1f04379974 | ||
|
|
e060c179f6 | ||
|
|
54b72ce2b8 | ||
|
|
5795b8787d |
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -44,8 +44,3 @@ GitHub Actions:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/**
|
||||
- .github/*.yml
|
||||
|
||||
Supervisor:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- hassio/src/**
|
||||
|
||||
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
@@ -101,27 +101,3 @@ jobs:
|
||||
path: hass_frontend/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
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@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend, supervisor]
|
||||
bundle: [frontend]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
29
.github/workflows/release.yaml
vendored
29
.github/workflows/release.yaml
vendored
@@ -118,32 +118,3 @@ jobs:
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
release-supervisor:
|
||||
name: Release supervisor frontend
|
||||
if: github.event.release.prerelease == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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
|
||||
|
||||
55
.vscode/tasks.json
vendored
55
.vscode/tasks.json
vendored
@@ -73,37 +73,6 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Supervisor panel",
|
||||
"type": "gulp",
|
||||
"task": "develop-hassio",
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": "build",
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Gallery",
|
||||
"type": "gulp",
|
||||
@@ -246,20 +215,6 @@
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run HA Core for Supervisor in devcontainer",
|
||||
"type": "shell",
|
||||
"command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core",
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Setup and fetch nightly translations",
|
||||
"type": "gulp",
|
||||
@@ -268,16 +223,6 @@
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "supervisorHost",
|
||||
"type": "promptString",
|
||||
"description": "The IP of the Supervisor host running the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
|
||||
@@ -14,7 +14,6 @@ This is the repository for the official [Home Assistant](https://home-assistant.
|
||||
- Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/)
|
||||
- Production build: `script/build_frontend`
|
||||
- Gallery: `cd gallery && script/develop_gallery`
|
||||
- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing)
|
||||
|
||||
## Frontend development
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ module.exports.sourceMapURL = () => {
|
||||
module.exports.ignorePackages = () => [];
|
||||
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
module.exports.emptyPackages = ({ isLandingPageBuild }) =>
|
||||
[
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
// Icons in landingpage conflict with icons in HA so we don't load.
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
|
||||
),
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
isLandingPageBuild &&
|
||||
require.resolve(
|
||||
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
|
||||
),
|
||||
@@ -36,7 +36,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__DEMO__: false,
|
||||
__SUPERVISOR__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
__HASS_URL__: `\`${
|
||||
@@ -289,26 +288,6 @@ module.exports.config = {
|
||||
};
|
||||
},
|
||||
|
||||
hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
|
||||
return {
|
||||
name: "supervisor" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
|
||||
},
|
||||
outputPath: outputPath(paths.hassio_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild: true,
|
||||
defineOverlay: {
|
||||
__SUPERVISOR__: true,
|
||||
__STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
gallery({ isProdBuild, latestBuild }) {
|
||||
return {
|
||||
name: "gallery" + nameSuffix(latestBuild),
|
||||
|
||||
@@ -24,10 +24,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("clean-hassio", async () =>
|
||||
deleteSync([paths.hassio_output_root, paths.build_dir])
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
|
||||
@@ -43,29 +43,11 @@ const compressAppModernBrotli = () =>
|
||||
const compressAppModernZopfli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressHassioModernBrotli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"brotli"
|
||||
);
|
||||
const compressHassioModernZopfli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"zopfli"
|
||||
);
|
||||
|
||||
const compressAppOtherBrotli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppOtherZopfli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressHassioOtherBrotli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
||||
const compressHassioOtherZopfli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
||||
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
@@ -75,12 +57,3 @@ gulp.task(
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
);
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(
|
||||
compressHassioModernBrotli,
|
||||
compressHassioOtherBrotli,
|
||||
compressHassioModernZopfli,
|
||||
compressHassioOtherZopfli
|
||||
)
|
||||
);
|
||||
|
||||
@@ -266,28 +266,3 @@ gulp.task(
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-hassio-dev",
|
||||
genPagesDevTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
"src",
|
||||
paths.hassio_publicPath
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-hassio-prod",
|
||||
genPagesProdTask(
|
||||
HASSIO_PAGE_ENTRIES,
|
||||
paths.hassio_dir,
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
paths.hassio_output_es5,
|
||||
"src"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -123,22 +123,11 @@ gulp.task("copy-translations-app", async () => {
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-landing-page", async () => {
|
||||
const staticDir = paths.landingPage_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
copyFonts(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-app", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
// Basic static files
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"gen-pages-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
"rspack-watch-hassio"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-hassio",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
"rspack-prod-hassio",
|
||||
"gen-pages-hassio-prod",
|
||||
...// Don't compress running tests
|
||||
(env.isTestBuild() ? [] : ["compress-hassio"])
|
||||
)
|
||||
);
|
||||
@@ -9,7 +9,6 @@ import "./fetch-nightly-translations.js";
|
||||
import "./gallery.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./hassio.js";
|
||||
import "./landing-page.js";
|
||||
import "./locale-data.js";
|
||||
import "./rspack.js";
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
createCastConfig,
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createHassioConfig,
|
||||
createLandingPageConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
@@ -159,31 +158,6 @@ gulp.task("rspack-prod-cast", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-watch-hassio", () => {
|
||||
// This command will run forever because we don't close compiler
|
||||
rspack(
|
||||
createHassioConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
})
|
||||
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("rspack-prod-hassio", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createHassioConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-gallery", () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
|
||||
@@ -170,9 +170,7 @@ const setFragment = (fragment) => async () => {
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" &&
|
||||
fragment !== "supervisor" &&
|
||||
fragment !== "landing-page";
|
||||
fragment !== "base" && fragment !== "landing-page";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
@@ -207,18 +205,15 @@ const createTranslations = async () => {
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
// Remove the panels and landing-page to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
supervisor: undefined,
|
||||
"landing-page": undefined,
|
||||
}),
|
||||
"",
|
||||
];
|
||||
case "supervisor":
|
||||
// Supervisor key is at the top level
|
||||
return [flatten(data.supervisor), ""];
|
||||
case "landing-page":
|
||||
// landing-page key is at the top level
|
||||
return [flatten(data["landing-page"]), ""];
|
||||
@@ -318,11 +313,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(setFragment("supervisor"), "build-translations")
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page-translations",
|
||||
gulp.series(setFragment("landing-page"), "build-translations")
|
||||
|
||||
@@ -49,15 +49,5 @@ module.exports = {
|
||||
"../landing-page/dist/static"
|
||||
),
|
||||
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
|
||||
hassio_output_latest: path.resolve(
|
||||
__dirname,
|
||||
"../hassio/build/frontend_latest"
|
||||
),
|
||||
hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"),
|
||||
hassio_publicPath: "/api/hassio/app",
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ const createRspackConfig = ({
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
isHassioBuild,
|
||||
isLandingPageBuild,
|
||||
dontHash,
|
||||
}) => {
|
||||
@@ -168,13 +167,9 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||
bundle.emptyPackages({ isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle
|
||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||
.join("|")
|
||||
),
|
||||
new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
@@ -326,21 +321,6 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
||||
|
||||
const createHassioConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}) =>
|
||||
createRspackConfig(
|
||||
bundle.config.hassio({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
})
|
||||
);
|
||||
|
||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
@@ -351,7 +331,6 @@ module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
|
||||
@@ -206,7 +206,7 @@ class HcCast extends LitElement {
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export default tseslint.config(
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
// Target directory for the build.
|
||||
buildDir: path.resolve(__dirname, "build"),
|
||||
nodeDir: path.resolve(__dirname, "../node_modules"),
|
||||
// Path where the Hass.io frontend will be publicly available.
|
||||
publicPath: "/api/hassio/app",
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Builds the Hass.io app for production
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-hassio
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Run the Hass.io development server
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-hassio
|
||||
@@ -1,149 +0,0 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } 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 { 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("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[];
|
||||
|
||||
@property() public filter!: string;
|
||||
|
||||
private _getAddons = memoizeOne((addons: StoreAddon[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repo = this.repo;
|
||||
let _addons = this.addons;
|
||||
if (!this.hass.userData?.showAdvanced) {
|
||||
_addons = _addons.filter(
|
||||
(addon) => !addon.advanced && addon.stage === "stable"
|
||||
);
|
||||
}
|
||||
const addons = this._getAddons(_addons, this.filter);
|
||||
|
||||
if (this.filter && addons.length < 1) {
|
||||
return html`
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
${this.supervisor.localize("store.no_results_found", {
|
||||
repository: repo.name,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>${repo.name}</h1>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
<ha-card
|
||||
outlined
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed && addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.update_available
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("app.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("app.state.not_installed")
|
||||
: this.supervisor.localize("app.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addonTapped(ev) {
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
a.repo {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-repository": HassioAddonRepositoryEl;
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/search-input";
|
||||
import type { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import "./hassio-addon-repository";
|
||||
|
||||
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
|
||||
if (a.slug === "local") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "local") {
|
||||
return 1;
|
||||
}
|
||||
if (a.slug === "core") {
|
||||
return -1;
|
||||
}
|
||||
if (b.slug === "core") {
|
||||
return 1;
|
||||
}
|
||||
return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1;
|
||||
};
|
||||
|
||||
@customElement("hassio-addon-store")
|
||||
export class HassioAddonStore extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public async refreshData() {
|
||||
try {
|
||||
await reloadHassioAddons(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let repos: (TemplateResult | typeof nothing)[] = [];
|
||||
|
||||
if (this.supervisor.store.repositories) {
|
||||
repos = this.addonRepositories(
|
||||
this.supervisor.store.repositories,
|
||||
this.supervisor.store.addons,
|
||||
this._filter
|
||||
);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.header=${this.supervisor.localize("panel.store")}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("store.check_updates")}
|
||||
</ha-list-item>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("store.repositories")}
|
||||
</ha-list-item>
|
||||
${this.hass.userData?.showAdvanced &&
|
||||
atLeastVersion(this.hass.config.version, 0, 117)
|
||||
? html`<ha-list-item>
|
||||
${this.supervisor.localize("store.registries")}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
${repos.length === 0
|
||||
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
</div>
|
||||
|
||||
${repos}
|
||||
`}
|
||||
${!this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<div class="advanced">
|
||||
<a href="/profile" target="_top">
|
||||
${this.supervisor.localize("store.missing_apps")}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const repositoryUrl = extractSearchParam("repository_url");
|
||||
navigate("/hassio/store", { replace: true });
|
||||
if (repositoryUrl) {
|
||||
this._manageRepositories(repositoryUrl);
|
||||
}
|
||||
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private addonRepositories = memoizeOne(
|
||||
(
|
||||
repositories: HassioAddonRepository[],
|
||||
addons: StoreAddon[],
|
||||
filter?: string
|
||||
) =>
|
||||
repositories.sort(sortRepos).map((repo) => {
|
||||
const filteredAddons = addons.filter(
|
||||
(addon) => addon.repository === repo.slug
|
||||
);
|
||||
|
||||
return filteredAddons.length !== 0
|
||||
? html`
|
||||
<hassio-addon-repository
|
||||
.hass=${this.hass}
|
||||
.repo=${repo}
|
||||
.addons=${filteredAddons}
|
||||
.filter=${filter!}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addon-repository>
|
||||
`
|
||||
: nothing;
|
||||
})
|
||||
);
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this.refreshData();
|
||||
break;
|
||||
case 1:
|
||||
this._manageRepositoriesClicked();
|
||||
break;
|
||||
case 2:
|
||||
this._manageRegistries();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _apiCalled(ev) {
|
||||
if (ev.detail.success) {
|
||||
this._loadData();
|
||||
}
|
||||
}
|
||||
|
||||
private _manageRepositoriesClicked() {
|
||||
this._manageRepositories();
|
||||
}
|
||||
|
||||
private _manageRepositories(url?: string) {
|
||||
showRepositoriesDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
private _manageRegistries() {
|
||||
showRegistriesDialog(this, { supervisor: this.supervisor });
|
||||
}
|
||||
|
||||
private _loadData() {
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "supervisor",
|
||||
});
|
||||
}
|
||||
|
||||
private _filterChanged(e) {
|
||||
this._filter = e.detail.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
.advanced {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.advanced a {
|
||||
margin-left: 0.5em;
|
||||
margin-inline-start: 0.5em;
|
||||
margin-inline-end: initial;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-store": HassioAddonStore;
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
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 "../../../../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("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;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _inputDevices?: HassioHardwareAudioDevice[];
|
||||
|
||||
@state() private _outputDevices?: HassioHardwareAudioDevice[];
|
||||
|
||||
@state() private _selectedInput!: null | string;
|
||||
|
||||
@state() private _selectedOutput!: null | string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("app.configuration.audio.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${this._inputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize("app.configuration.audio.input")}
|
||||
@selected=${this._setInputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedInput!}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this._inputDevices.map(
|
||||
(item) => html`
|
||||
<ha-list-item .value=${item.device || ""}>
|
||||
${item.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
${this._outputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize("app.configuration.audio.output")}
|
||||
@selected=${this._setOutputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this._selectedOutput!}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this._outputDevices.map(
|
||||
(item) => html`
|
||||
<ha-list-item .value=${item.device || ""}
|
||||
>${item.name}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._saveSettings}
|
||||
>
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host,
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-select:last-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._addonChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _setInputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
this._selectedInput = device;
|
||||
}
|
||||
|
||||
private _setOutputDevice(ev): void {
|
||||
const device = ev.target.value;
|
||||
this._selectedOutput = device;
|
||||
}
|
||||
|
||||
private async _addonChanged(): Promise<void> {
|
||||
this._selectedInput =
|
||||
this.addon.audio_input === null ? "default" : this.addon.audio_input;
|
||||
this._selectedOutput =
|
||||
this.addon.audio_output === null ? "default" : this.addon.audio_output;
|
||||
if (this._outputDevices) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: this.supervisor.localize("app.configuration.audio.default"),
|
||||
};
|
||||
|
||||
try {
|
||||
const { audio } = await fetchHassioHardwareAudio(this.hass);
|
||||
const input = Object.keys(audio.input).map((key) => ({
|
||||
device: key,
|
||||
name: audio.input[key],
|
||||
}));
|
||||
const output = Object.keys(audio.output).map((key) => ({
|
||||
device: key,
|
||||
name: audio.output[key],
|
||||
}));
|
||||
|
||||
this._inputDevices = [noDevice, ...input];
|
||||
this._outputDevices = [noDevice, ...output];
|
||||
} catch {
|
||||
this._error = "Failed to fetch audio hardware";
|
||||
this._inputDevices = [noDevice];
|
||||
this._outputDevices = [noDevice];
|
||||
}
|
||||
}
|
||||
|
||||
private async _saveSettings(ev: CustomEvent): Promise<void> {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
audio_input:
|
||||
this._selectedInput === "default" ? null : this._selectedInput,
|
||||
audio_output:
|
||||
this._selectedOutput === "default" ? null : this._selectedOutput,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch {
|
||||
this._error = "Failed to set addon audio device";
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-audio": HassioAddonAudio;
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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("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;
|
||||
|
||||
@property({ type: Boolean, attribute: "control-enabled" })
|
||||
public controlEnabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
const hasConfiguration =
|
||||
(this.addon.options && Object.keys(this.addon.options).length) ||
|
||||
(this.addon.schema && Object.keys(this.addon.schema).length);
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
${this.addon.system_managed &&
|
||||
(hasConfiguration || this.addon.network || this.addon.audio)
|
||||
? html`
|
||||
<hassio-addon-system-managed
|
||||
.supervisor=${this.supervisor}
|
||||
.narrow=${this.narrow}
|
||||
.hideButton=${this.controlEnabled}
|
||||
></hassio-addon-system-managed>
|
||||
`
|
||||
: nothing}
|
||||
${hasConfiguration || this.addon.network || this.addon.audio
|
||||
? html`
|
||||
${hasConfiguration
|
||||
? html`
|
||||
<hassio-addon-config
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></hassio-addon-config>
|
||||
`
|
||||
: nothing}
|
||||
${this.addon.network
|
||||
? html`
|
||||
<hassio-addon-network
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></hassio-addon-network>
|
||||
`
|
||||
: nothing}
|
||||
${this.addon.audio
|
||||
? html`
|
||||
<hassio-addon-audio
|
||||
.hass=${this.hass}
|
||||
.addon=${this.addon}
|
||||
.supervisor=${this.supervisor}
|
||||
.disabled=${this.addon.system_managed &&
|
||||
!this.controlEnabled}
|
||||
></hassio-addon-audio>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: this.supervisor.localize("app.configuration.no_configuration")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
hassio-addon-network,
|
||||
hassio-addon-audio,
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-config-tab": HassioAddonConfigDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
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 "../../../../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 {
|
||||
HaFormSchema,
|
||||
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 "../../../../src/data/hassio/addon";
|
||||
import {
|
||||
setHassioAddonOption,
|
||||
validateHassioAddonOption,
|
||||
} 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",
|
||||
"select",
|
||||
"boolean",
|
||||
"integer",
|
||||
"float",
|
||||
"schema",
|
||||
];
|
||||
|
||||
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
new Type("!secret", {
|
||||
kind: "scalar",
|
||||
construct: (data) => `!secret ${data}`,
|
||||
}),
|
||||
]);
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@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;
|
||||
|
||||
@state() private _valid = true;
|
||||
|
||||
@state() private _canShowSchema = false;
|
||||
|
||||
@state() private _showOptional = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _options?: Record<string, unknown>;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
private _getTranslationEntry(
|
||||
language: string,
|
||||
entry: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) {
|
||||
let parent = this.addon.translations[language]?.configuration;
|
||||
if (!parent) return undefined;
|
||||
if (options?.path) {
|
||||
for (const key of options.path) {
|
||||
parent = parent[key]?.fields;
|
||||
if (!parent) return undefined;
|
||||
}
|
||||
}
|
||||
return parent[entry.name];
|
||||
}
|
||||
|
||||
public computeLabel = (
|
||||
entry: HaFormSchema,
|
||||
_data: HaFormDataContainer,
|
||||
options?: { path?: string[] }
|
||||
): string =>
|
||||
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
|
||||
this._getTranslationEntry("en", entry, options)?.name ||
|
||||
entry.name;
|
||||
|
||||
public computeHelper = (
|
||||
entry: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
): string =>
|
||||
this._getTranslationEntry(this.hass.language, entry, options)
|
||||
?.description ||
|
||||
this._getTranslationEntry("en", entry, options)?.description ||
|
||||
"";
|
||||
|
||||
private _convertSchema = memoizeOne(
|
||||
// Convert supervisor schema to selectors
|
||||
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
|
||||
this._convertSchemaElements(schema)
|
||||
);
|
||||
|
||||
private _convertSchemaElements(
|
||||
schema: readonly HaFormSchema[]
|
||||
): HaFormSchema[] {
|
||||
return schema.map((entry) => this._convertSchemaElement(entry));
|
||||
}
|
||||
|
||||
private _convertSchemaElement(entry: any): HaFormSchema {
|
||||
if (entry.type === "schema" && !entry.multiple) {
|
||||
return {
|
||||
name: entry.name,
|
||||
type: "expandable",
|
||||
required: entry.required,
|
||||
schema: this._convertSchemaElements(entry.schema),
|
||||
};
|
||||
}
|
||||
const selector = this._convertSchemaElementToSelector(entry, false);
|
||||
if (selector) {
|
||||
return {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector,
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private _convertSchemaElementToSelector(
|
||||
entry: any,
|
||||
force: boolean
|
||||
): Selector | null {
|
||||
if (entry.type === "select") {
|
||||
return { select: { options: entry.options } };
|
||||
}
|
||||
if (entry.type === "string") {
|
||||
return entry.multiple
|
||||
? { select: { options: [], multiple: true, custom_value: true } }
|
||||
: {
|
||||
text: {
|
||||
type: entry.format
|
||||
? entry.format
|
||||
: MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (entry.type === "boolean") {
|
||||
return { boolean: {} };
|
||||
}
|
||||
if (entry.type === "schema") {
|
||||
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
|
||||
for (const child_entry of entry.schema) {
|
||||
fields[child_entry.name] = {
|
||||
required: child_entry.required,
|
||||
selector: this._convertSchemaElementToSelector(child_entry, true)!,
|
||||
};
|
||||
}
|
||||
return {
|
||||
object: {
|
||||
multiple: entry.multiple,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (entry.type === "float" || entry.type === "integer") {
|
||||
return {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: entry.type === "float" ? "any" : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (force) {
|
||||
return { object: {} };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _filteredSchema = memoizeOne(
|
||||
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
|
||||
schema.filter((entry) => entry.name in options || entry.required)
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showForm =
|
||||
!this._yamlMode && this._canShowSchema && this.addon.schema;
|
||||
const hasHiddenOptions =
|
||||
showForm &&
|
||||
JSON.stringify(this.addon.schema) !==
|
||||
JSON.stringify(
|
||||
this._filteredSchema(this.addon.options, this.addon.schema!)
|
||||
);
|
||||
return html`
|
||||
<h1>${this.addon.name}</h1>
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.supervisor.localize("app.configuration.options.header")}
|
||||
</h2>
|
||||
<div class="card-menu">
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
|
||||
${this._yamlMode
|
||||
? this.supervisor.localize(
|
||||
"app.configuration.options.edit_in_ui"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"app.configuration.options.edit_in_yaml"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class=${!this.disabled ? "warning" : ""}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.supervisor.localize("common.reset_defaults")}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
${showForm
|
||||
? html`<ha-form
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.data=${this._options!}
|
||||
@value-changed=${this._configChanged}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.schema=${this._convertSchema(
|
||||
this._showOptional
|
||||
? this.addon.schema!
|
||||
: this._filteredSchema(
|
||||
this.addon.options,
|
||||
this.addon.schema!
|
||||
)
|
||||
)}
|
||||
></ha-form>`
|
||||
: html`<ha-yaml-editor
|
||||
@value-changed=${this._configChanged}
|
||||
.yamlSchema=${ADDON_YAML_SCHEMA}
|
||||
></ha-yaml-editor>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${!this._yamlMode ||
|
||||
(this._canShowSchema && this.addon.schema) ||
|
||||
this._valid
|
||||
? ""
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"app.configuration.options.invalid_yaml"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
</div>
|
||||
${hasHiddenOptions
|
||||
? html`<ha-formfield
|
||||
class="show-additional"
|
||||
.label=${this.supervisor.localize(
|
||||
"app.configuration.options.show_unused_optional"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._toggleOptional}
|
||||
.checked=${this._showOptional}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
<div class="card-actions right">
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${this.disabled ||
|
||||
!this._configHasChanged ||
|
||||
!this._valid}
|
||||
>
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._canShowSchema = !this.addon.schema!.find(
|
||||
(entry) =>
|
||||
// @ts-ignore
|
||||
!SUPPORTED_UI_TYPES.includes(entry.type)
|
||||
);
|
||||
this._yamlMode = !this._canShowSchema;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("addon")) {
|
||||
this._options = { ...this.addon.options };
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("_yamlMode") ||
|
||||
changedProperties.has("_options")
|
||||
) {
|
||||
if (this._yamlMode) {
|
||||
const editor = this._editor;
|
||||
if (editor) {
|
||||
editor.setValue(this._options!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case 1:
|
||||
this._resetTapped(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleOptional() {
|
||||
this._showOptional = !this._showOptional;
|
||||
}
|
||||
|
||||
private _configChanged(ev): void {
|
||||
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
|
||||
this._valid = true;
|
||||
this._configHasChanged = true;
|
||||
this._options = ev.detail.value;
|
||||
} else {
|
||||
this._configHasChanged = true;
|
||||
this._valid = ev.detail.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
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,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
options: null,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (this.disabled || !this._configHasChanged || !this._valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
const options: Record<string, unknown> = this._yamlMode
|
||||
? this._editor?.value
|
||||
: this._options;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "options",
|
||||
};
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
const validation = await validateHassioAddonOption(
|
||||
this.hass,
|
||||
this.addon.slug,
|
||||
options
|
||||
);
|
||||
if (!validation.valid) {
|
||||
throw Error(validation.message);
|
||||
}
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, {
|
||||
options,
|
||||
});
|
||||
|
||||
this._configHasChanged = false;
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
eventdata.success = false;
|
||||
}
|
||||
button.progress = false;
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-menu {
|
||||
float: right;
|
||||
z-index: 3;
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header h2 {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: 12px 16px 16px;
|
||||
display: block;
|
||||
margin-block: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
.card-actions.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.show-additional {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-config": HassioAddonConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
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 "../../../../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 "../../../../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("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;
|
||||
|
||||
@state() private _showOptional = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _config?: Record<string, any>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setNetworkConfig();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasHiddenOptions = Object.keys(this._config).find(
|
||||
(entry) => this._config![entry] === null
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("app.configuration.network.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"app.configuration.network.introduction"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<ha-form
|
||||
.disabled=${this.disabled}
|
||||
.data=${this._config}
|
||||
@value-changed=${this._configChanged}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.schema=${this._createSchema(
|
||||
this._config,
|
||||
this._showOptional,
|
||||
this.hass.userData?.showAdvanced || false
|
||||
)}
|
||||
></ha-form>
|
||||
</div>
|
||||
${hasHiddenOptions
|
||||
? html`<ha-formfield
|
||||
class="show-optional"
|
||||
.label=${this.supervisor.localize(
|
||||
"app.configuration.network.show_disabled"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._toggleOptional}
|
||||
.checked=${this._showOptional}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>`
|
||||
: nothing}
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._resetTapped}
|
||||
>
|
||||
${this.supervisor.localize("common.reset_defaults")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || this.disabled}
|
||||
>
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues): void {
|
||||
super.update(changedProperties);
|
||||
if (changedProperties.has("addon")) {
|
||||
this._setNetworkConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private _createSchema = memoizeOne(
|
||||
(
|
||||
config: Record<string, number>,
|
||||
showOptional: boolean,
|
||||
advanced: boolean
|
||||
): HaFormSchema[] =>
|
||||
(showOptional
|
||||
? Object.keys(config)
|
||||
: Object.keys(config).filter((entry) => config[entry] !== null)
|
||||
).map((entry) => ({
|
||||
name: entry,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
min: 0,
|
||||
max: 65535,
|
||||
unit_of_measurement: advanced ? entry : undefined,
|
||||
},
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
private _computeLabel = (_: HaFormSchema): string => "";
|
||||
|
||||
private _computeHelper = (item: HaFormSchema): string =>
|
||||
this.addon.translations[this.hass.language]?.network?.[item.name] ||
|
||||
this.addon.translations.en?.network?.[item.name] ||
|
||||
this.addon.network_description?.[item.name] ||
|
||||
item.name;
|
||||
|
||||
private _setNetworkConfig(): void {
|
||||
this._config = this.addon.network || {};
|
||||
}
|
||||
|
||||
private async _configChanged(ev: CustomEvent): Promise<void> {
|
||||
this._configHasChanged = true;
|
||||
this._config = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
network: null,
|
||||
};
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleOptional() {
|
||||
this._showOptional = !this._showOptional;
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (!this._configHasChanged || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
|
||||
this._error = undefined;
|
||||
const networkconfiguration = {};
|
||||
Object.entries(this._config!).forEach(([key, value]) => {
|
||||
networkconfiguration[key] = value ?? null;
|
||||
});
|
||||
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
network: networkconfiguration,
|
||||
};
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "option",
|
||||
};
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.show-optional {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-network": HassioAddonNetwork;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import "../../../../src/components/ha-card";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
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 "../../../../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("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;
|
||||
|
||||
@state() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
? html`<ha-markdown
|
||||
.content=${this._content}
|
||||
lazy-images
|
||||
></ha-markdown>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
display: block;
|
||||
}
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
try {
|
||||
this._content = await fetchHassioAddonDocumentation(
|
||||
this.hass,
|
||||
this.addon!.slug
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"app.documentation.get_documentation",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-documentation-tab": HassioAddonDocumentationDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
import {
|
||||
mdiCogs,
|
||||
mdiFileDocument,
|
||||
mdiInformationVariant,
|
||||
mdiTextBoxOutline,
|
||||
} 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: "app.panel.info",
|
||||
path: `/hassio/addon/${this.addon.slug}/info`,
|
||||
iconPath: mdiInformationVariant,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.addon.documentation) {
|
||||
addonTabs.push({
|
||||
translationKey: "app.panel.documentation",
|
||||
path: `/hassio/addon/${this.addon.slug}/documentation`,
|
||||
iconPath: mdiFileDocument,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addon.version) {
|
||||
addonTabs.push(
|
||||
{
|
||||
translationKey: "app.panel.configuration",
|
||||
path: `/hassio/addon/${this.addon.slug}/config`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
{
|
||||
translationKey: "app.panel.log",
|
||||
path: `/hassio/addon/${this.addon.slug}/logs`,
|
||||
iconPath: mdiTextBoxOutline,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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_app_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_app_repository_description",
|
||||
{ app: 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_app_not_found");
|
||||
} else {
|
||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
|
||||
}
|
||||
|
||||
private async _apiCalled(ev): Promise<void> {
|
||||
if (!ev.detail.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSplit: string[] = ev.detail.path?.split("/");
|
||||
|
||||
if (!pathSplit || pathSplit.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path: string = pathSplit[pathSplit.length - 1];
|
||||
|
||||
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
}
|
||||
|
||||
if (path === "uninstall") {
|
||||
if (this.isConnected) {
|
||||
navigate(this._backPath);
|
||||
}
|
||||
} else if (path === "install") {
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
|
||||
} else {
|
||||
await this._routeDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("route") && !this.addon) {
|
||||
this._routeDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async _routeDataChanged(): Promise<void> {
|
||||
const addon = this.route.path.split("/")[1];
|
||||
if (!addon) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!this.supervisor.addon) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { addon: addonsInfo });
|
||||
}
|
||||
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
|
||||
} catch (err: any) {
|
||||
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
|
||||
this.addon = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-dashboard": HassioAddonDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import type { StoreAddonDetails } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import type { RouterOptions } from "../../../src/layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../src/layouts/hass-router-page";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./config/hassio-addon-config-tab";
|
||||
import "./documentation/hassio-addon-documentation-tab";
|
||||
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
|
||||
import "./info/hassio-addon-info-tab";
|
||||
import "./log/hassio-addon-log-tab";
|
||||
|
||||
@customElement("hassio-addon-router")
|
||||
class HassioAddonRouter extends HassRouterPage {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ type: Boolean, attribute: "control-enabled" })
|
||||
public controlEnabled = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "info",
|
||||
showLoading: true,
|
||||
routes: {
|
||||
info: {
|
||||
tag: "hassio-addon-info-tab",
|
||||
},
|
||||
documentation: {
|
||||
tag: "hassio-addon-documentation-tab",
|
||||
},
|
||||
config: {
|
||||
tag: "hassio-addon-config-tab",
|
||||
},
|
||||
logs: {
|
||||
tag: "hassio-addon-log-tab",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
el.addon = this.addon;
|
||||
el.narrow = this.narrow;
|
||||
el.controlEnabled = this.controlEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-router": HassioAddonRouter;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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("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" })
|
||||
public controlEnabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.addon=${this.addon}
|
||||
.controlEnabled=${this.controlEnabled}
|
||||
></hassio-addon-info>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-info-tab": HassioAddonInfoDashboard;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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("hassio-addon-system-managed")
|
||||
class HassioAddonSystemManaged extends LitElement {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-button" }) public hideButton =
|
||||
false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.supervisor.localize("app.system_managed.title")}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${this.supervisor.localize("app.system_managed.description")}
|
||||
${!this.hideButton
|
||||
? html`
|
||||
<ha-button slot="action" @click=${this._takeControl}>
|
||||
${this.supervisor.localize("app.system_managed.take_control")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
private _takeControl() {
|
||||
fireEvent(this, "system-managed-take-control");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-system-managed": HassioAddonSystemManaged;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"system-managed-take-control": undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
type CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
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("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") || "";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html` <ha-spinner></ha-spinner> `;
|
||||
}
|
||||
return html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.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}
|
||||
>
|
||||
</error-log-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
@media all and (max-width: 870px) {
|
||||
:host {
|
||||
--error-log-card-height: calc(100vh - 304px);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-log-tab": HassioAddonLogDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
|
||||
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { relativeTime } from "../../../src/common/datetime/relative_time";
|
||||
import type { HASSDomEvent } from "../../../src/common/dom/fire_event";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../src/components/data-table/ha-data-table";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-fab";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import {
|
||||
fetchHassioBackups,
|
||||
friendlyFolderName,
|
||||
reloadHassioBackups,
|
||||
removeBackup,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
|
||||
import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location";
|
||||
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
|
||||
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
type BackupItem = HassioBackup & {
|
||||
secondary: string;
|
||||
};
|
||||
|
||||
@customElement("hassio-backups")
|
||||
export class HassioBackups extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _selectedBackups: string[] = [];
|
||||
|
||||
@state() private _backups?: HassioBackup[] = [];
|
||||
|
||||
@state() private _isLoading = false;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this._firstUpdatedCalled) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private _computeBackupContent = (backup: HassioBackup): string => {
|
||||
if (backup.type === "full") {
|
||||
return this.supervisor.localize("backup.full_backup");
|
||||
}
|
||||
const content: string[] = [];
|
||||
if (backup.content.homeassistant) {
|
||||
content.push("Home Assistant");
|
||||
}
|
||||
if (backup.content.folders.length !== 0) {
|
||||
for (const folder of backup.content.folders) {
|
||||
content.push(friendlyFolderName[folder] || folder);
|
||||
}
|
||||
}
|
||||
|
||||
if (backup.content.addons.length !== 0) {
|
||||
for (const addon of backup.content.addons) {
|
||||
content.push(
|
||||
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
|
||||
?.name || addon
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return content.join(", ");
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass && this.isConnected) {
|
||||
this._fetchBackups();
|
||||
}
|
||||
this._firstUpdatedCalled = true;
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<BackupItem> => ({
|
||||
name: {
|
||||
title: this.supervisor.localize("backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
template: (backup) =>
|
||||
html`${backup.name || backup.slug}
|
||||
<div class="secondary">${backup.secondary}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: this.supervisor.localize("backup.size"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
backup.location || this.supervisor.localize("backup.data_disk"),
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
secondary: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] =>
|
||||
backups.map((backup) => ({
|
||||
...backup,
|
||||
secondary: this._computeBackupContent(backup),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._isLoading) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this.supervisor.localize("backup.loading_backups")}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? [
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
]
|
||||
: supervisorTabs(this.hass)}
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.searchLabel=${this.supervisor.localize("backup.search")}
|
||||
.noDataText=${this.supervisor.localize("backup.no_backups")}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.data=${this._backupData(this._backups || [])}
|
||||
id="slug"
|
||||
@row-click=${this._handleRowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
selectable
|
||||
has-fab
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
|
||||
? "/config/system"
|
||||
: "/config"}
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor?.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("common.reload")}
|
||||
</ha-list-item>
|
||||
<ha-list-item>
|
||||
${this.supervisor.localize("dialog.backup_location.title")}
|
||||
</ha-list-item>
|
||||
${atLeastVersion(this.hass.config.version, 0, 116)
|
||||
? html`<ha-list-item>
|
||||
${this.supervisor.localize("backup.upload_backup")}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
|
||||
${this._selectedBackups.length
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.supervisor.localize("backup.selected", {
|
||||
number: this._selectedBackups.length,
|
||||
})}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
@click=${this._deleteSelected}
|
||||
>
|
||||
${this.supervisor.localize("backup.delete_selected")}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize(
|
||||
"backup.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</div>
|
||||
</div> `
|
||||
: ""}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
@click=${this._createBackup}
|
||||
.label=${this.supervisor.localize("backup.create_backup")}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._fetchBackups();
|
||||
break;
|
||||
case 1:
|
||||
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
|
||||
break;
|
||||
case 2:
|
||||
this._showUploadBackupDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedBackups = ev.detail.value;
|
||||
}
|
||||
|
||||
private _showUploadBackupDialog() {
|
||||
showBackupUploadDialog(this, {
|
||||
showBackup: (slug: string) =>
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
}),
|
||||
reloadBackup: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _fetchBackups() {
|
||||
this._isLoading = true;
|
||||
await reloadHassioBackups(this.hass);
|
||||
this._backups = await fetchHassioBackups(this.hass);
|
||||
this._isLoading = false;
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("backup.delete_backup_title"),
|
||||
text: this.supervisor.localize("backup.delete_backup_text", {
|
||||
number: this._selectedBackups.length,
|
||||
}),
|
||||
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this._selectedBackups.map((slug) => removeBackup(this.hass, slug))
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("backup.failed_to_delete"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this._fetchBackups();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const slug = ev.detail.id;
|
||||
showHassioBackupDialog(this, {
|
||||
slug,
|
||||
supervisor: this.supervisor,
|
||||
onDelete: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
private _createBackup() {
|
||||
if (this.supervisor!.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor!.localize("backup.could_not_create"),
|
||||
text: this.supervisor!.localize("backup.create_blocked_not_running", {
|
||||
state: this.supervisor!.info.state,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioCreateBackupDialog(this, {
|
||||
supervisor: this.supervisor!,
|
||||
onCreate: () => this._fetchBackups(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 58px;
|
||||
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
|
||||
}
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: initial;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.header-toolbar .selected-txt {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-backups": HassioBackups;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@customElement("hassio-card-content")
|
||||
class HassioCardContent extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ type: Boolean }) public available = true;
|
||||
|
||||
@property({ attribute: false }) public showTopbar = false;
|
||||
|
||||
@property({ attribute: false }) public topbarClass?: string;
|
||||
|
||||
@property({ attribute: false }) public iconTitle?: string;
|
||||
|
||||
@property({ attribute: false }) public iconClass?: string;
|
||||
|
||||
@property() public icon = mdiHelpCircle;
|
||||
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.showTopbar
|
||||
? html` <div class="topbar ${this.topbarClass}"></div> `
|
||||
: ""}
|
||||
${this.iconImage
|
||||
? html`
|
||||
<div class="icon_image ${this.iconClass}">
|
||||
<img
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
class=${this.iconClass!}
|
||||
.path=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<div>
|
||||
<div class="title">${this.title}</div>
|
||||
<div class="addition">
|
||||
${this.description}
|
||||
${
|
||||
/* treat as available when undefined */
|
||||
this.available === false ? " (Not available)" : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 12px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-svg-icon.update {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
ha-svg-icon.running,
|
||||
ha-svg-icon.installed {
|
||||
color: var(--success-color);
|
||||
}
|
||||
ha-svg-icon.hassupdate,
|
||||
ha-svg-icon.backup {
|
||||
color: var(--state-icon-color);
|
||||
}
|
||||
ha-svg-icon.not_available {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
margin-top: 4px;
|
||||
margin-right: 16px;
|
||||
float: left;
|
||||
}
|
||||
.icon_image.stopped,
|
||||
.icon_image.not_available {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
background-color: var(--warning-color);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.topbar.installed {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.topbar.update {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.topbar.unavailable {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-card-content": HassioCardContent;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
keys: ["name", "description", "slug"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import { uploadBackup } from "../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hassio-upload-backup")
|
||||
export class HassioUploadBackup extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() public value: string | null = null;
|
||||
|
||||
@state() private _uploading = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept="application/x-tar"
|
||||
.label=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}
|
||||
.supports=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_supports"
|
||||
) || "Supports .TAR files"}
|
||||
.secondary=${this.localize?.(
|
||||
"ui.panel.page-onboarding.restore.upload_drop"
|
||||
) || "Or drop your file here"}
|
||||
@file-picked=${this._uploadFile}
|
||||
@files-cleared=${this._clear}
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this.value = null;
|
||||
fireEvent(this, "backup-cleared");
|
||||
}
|
||||
|
||||
private async _uploadFile(ev) {
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (!["application/x-tar"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
text: "Please choose a Home Assistant backup file (.tar)",
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
text: extractApiErrorMessage(err),
|
||||
confirmText: "ok",
|
||||
});
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-upload-backup": HassioUploadBackup;
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
import { mdiFolder, mdiPuzzle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-password-field";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import type {
|
||||
HassioBackupDetail,
|
||||
HassioFullBackupCreateParams,
|
||||
HassioPartialBackupCreateParams,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AddonCheckboxItem extends CheckboxItem {
|
||||
version: string;
|
||||
}
|
||||
|
||||
const _computeFolders = (folders): CheckboxItem[] => {
|
||||
const list: CheckboxItem[] = [];
|
||||
if (folders.includes("ssl")) {
|
||||
list.push({ slug: "ssl", name: "SSL", checked: false });
|
||||
}
|
||||
if (folders.includes("share")) {
|
||||
list.push({ slug: "share", name: "Share", checked: false });
|
||||
}
|
||||
if (folders.includes("media")) {
|
||||
list.push({ slug: "media", name: "Media", checked: false });
|
||||
}
|
||||
if (folders.includes("addons/local")) {
|
||||
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
|
||||
}
|
||||
return list.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
};
|
||||
|
||||
const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
addons
|
||||
.map((addon) => ({
|
||||
slug: addon.slug,
|
||||
name: addon.name,
|
||||
version: addon.version,
|
||||
checked: false,
|
||||
}))
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
@customElement("supervisor-backup-content")
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
|
||||
@property({ attribute: false })
|
||||
public backupType: HassioBackupDetail["type"] = "full";
|
||||
|
||||
@property({ attribute: false }) public folders?: CheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
|
||||
|
||||
@property({ attribute: false }) public homeAssistant = false;
|
||||
|
||||
@property({ attribute: false }) public backupHasPassword = false;
|
||||
|
||||
@property({ type: Boolean }) public onboarding = false;
|
||||
|
||||
@property({ attribute: false }) public backupName = "";
|
||||
|
||||
@property({ attribute: false }) public backupPassword = "";
|
||||
|
||||
@property({ attribute: false }) public confirmBackupPassword = "";
|
||||
|
||||
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this.folders = _computeFolders(
|
||||
this.backup
|
||||
? this.backup.folders
|
||||
: ["ssl", "share", "media", "addons/local"]
|
||||
);
|
||||
this.addons = _computeAddons(
|
||||
this.backup ? this.backup.addons : this.supervisor?.addon.addons
|
||||
);
|
||||
this.backupType = this.backup?.type || "full";
|
||||
this.backupName = this.backup?.name || "";
|
||||
this.backupHasPassword = this.backup?.protected || false;
|
||||
}
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
const foldersSection =
|
||||
this.backupType === "partial" ? this._getSection("folders") : undefined;
|
||||
const addonsSection =
|
||||
this.backupType === "partial" ? this._getSection("addons") : undefined;
|
||||
|
||||
return html`
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
new Date(this.backup.date),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: this.backup.date}
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>`}
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
name="backupType"
|
||||
.checked=${this.backupType === "full"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
name="backupType"
|
||||
.checked=${this.backupType === "partial"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.backupType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
${!this.backup || this.backup.homeassistant
|
||||
? html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass?.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.onboarding || this.homeAssistant}
|
||||
.disabled=${this.onboarding}
|
||||
@change=${this._toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${foldersSection.checked}
|
||||
.indeterminate=${foldersSection.indeterminate}
|
||||
.section=${"folders"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${foldersSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
${addonsSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.apps")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._toggleSection}
|
||||
.checked=${addonsSection.checked}
|
||||
.indeterminate=${addonsSection.indeterminate}
|
||||
.section=${"addons"}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="section-content">${addonsSection.templates}</div>
|
||||
`
|
||||
: ""}
|
||||
</div> `
|
||||
: ""}
|
||||
${this.backupType === "partial" &&
|
||||
(!this.backup || this.backupHasPassword)
|
||||
? html`<hr />`
|
||||
: ""}
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@change=${this._toggleHasPassword}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-password-field>`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleHomeAssistant() {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.partial-picker ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
.partial-picker ha-checkbox {
|
||||
--mdc-checkbox-touch-target-size: 32px;
|
||||
}
|
||||
.partial-picker {
|
||||
display: block;
|
||||
margin: 0px -6px;
|
||||
}
|
||||
supervisor-formfield-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.section-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 30px;
|
||||
margin-inline-start: 30px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
ha-formfield.password {
|
||||
display: block;
|
||||
margin: 0 -14px -16px;
|
||||
}
|
||||
.backup-types {
|
||||
display: flex;
|
||||
margin-left: -13px;
|
||||
margin-inline-start: -13px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.sub-header {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
public backupDetails():
|
||||
| HassioPartialBackupCreateParams
|
||||
| HassioFullBackupCreateParams {
|
||||
const data: any = {};
|
||||
|
||||
if (!this.backup && this.hass) {
|
||||
data.name =
|
||||
this.backupName ||
|
||||
formatDate(new Date(), this.hass.locale, this.hass.config);
|
||||
}
|
||||
|
||||
if (this.backupHasPassword) {
|
||||
data.password = this.backupPassword;
|
||||
if (!this.backup) {
|
||||
data.confirm_password = this.confirmBackupPassword;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.backupType === "full") {
|
||||
return data;
|
||||
}
|
||||
|
||||
const addons = this.addons
|
||||
?.filter((addon) => addon.checked)
|
||||
.map((addon) => addon.slug);
|
||||
const folders = this.folders
|
||||
?.filter((folder) => folder.checked)
|
||||
.map((folder) => folder.slug);
|
||||
|
||||
if (addons?.length) {
|
||||
data.addons = addons;
|
||||
}
|
||||
if (folders?.length) {
|
||||
data.folders = folders;
|
||||
}
|
||||
|
||||
// onboarding needs at least homeassistant to restore
|
||||
data.homeassistant = this.onboarding || this.homeAssistant;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private _getSection(section: string) {
|
||||
const templates: TemplateResult[] = [];
|
||||
const addons =
|
||||
section === "addons"
|
||||
? new Map(
|
||||
this.supervisor?.addon.addons.map((item) => [item.slug, item])
|
||||
)
|
||||
: undefined;
|
||||
let checkedItems = 0;
|
||||
this[section].forEach((item) => {
|
||||
templates.push(
|
||||
html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${item.name}
|
||||
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
|
||||
.imageUrl=${section === "addons" &&
|
||||
!this.onboarding &&
|
||||
this.hass &&
|
||||
atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
addons?.get(item.slug)?.icon
|
||||
? `/api/hassio/addons/${item.slug}/icon`
|
||||
: undefined}
|
||||
.version=${item.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.item=${item}
|
||||
.checked=${item.checked}
|
||||
.section=${section}
|
||||
@change=${this._updateSectionEntry}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
);
|
||||
|
||||
if (item.checked) {
|
||||
checkedItems++;
|
||||
}
|
||||
});
|
||||
|
||||
const checked = checkedItems === this[section].length;
|
||||
|
||||
return {
|
||||
templates,
|
||||
checked,
|
||||
indeterminate: !checked && checkedItems !== 0,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent) {
|
||||
const input = ev.currentTarget as HaRadio;
|
||||
this[input.name] = input.value;
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: InputEvent) {
|
||||
const input = ev.currentTarget as HaTextField;
|
||||
this[input.name!] = input.value;
|
||||
}
|
||||
|
||||
private _toggleHasPassword(): void {
|
||||
this.backupHasPassword = !this.backupHasPassword;
|
||||
}
|
||||
|
||||
private _toggleSection(ev): void {
|
||||
const section = ev.currentTarget.section;
|
||||
|
||||
this[section] = (section === "addons" ? this.addons : this.folders)!.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
checked: ev.currentTarget.checked,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSectionEntry(ev): void {
|
||||
const item = ev.currentTarget.item;
|
||||
const section = ev.currentTarget.section;
|
||||
this[section] = this[section].map((entry) =>
|
||||
entry.slug === item.slug
|
||||
? {
|
||||
...entry,
|
||||
checked: ev.currentTarget.checked,
|
||||
}
|
||||
: entry
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-backup-content": SupervisorBackupContent;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
|
||||
@customElement("supervisor-formfield-label")
|
||||
class SupervisorFormfieldLabel extends LitElement {
|
||||
@property({ type: String }) public label!: string;
|
||||
|
||||
@property({ attribute: false }) public imageUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public iconPath?: string;
|
||||
|
||||
@property({ type: String }) public version?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.imageUrl
|
||||
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
|
||||
: this.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
class="icon"
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
<span class="label">${this.label}</span>
|
||||
${this.version
|
||||
? html`<span class="version">(${this.version})</span>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.version {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
max-height: 22px;
|
||||
max-width: 22px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-formfield-label": SupervisorFormfieldLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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 "../../../src/components/ha-bar";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { roundWithOneDecimal } from "../../../src/util/calculate";
|
||||
|
||||
@customElement("supervisor-metric")
|
||||
class SupervisorMetric extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
|
||||
@property({ type: String }) public description!: string;
|
||||
|
||||
@property({ type: String }) public tooltip?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const roundedValue = roundWithOneDecimal(this.value);
|
||||
return html`<ha-settings-row>
|
||||
<span slot="heading"> ${this.description} </span>
|
||||
<div slot="description" .title=${this.tooltip ?? ""}>
|
||||
<span class="value"> ${roundedValue} % </span>
|
||||
<ha-bar
|
||||
class=${classMap({
|
||||
"target-warning": roundedValue > 50,
|
||||
"target-critical": roundedValue > 85,
|
||||
})}
|
||||
.value=${this.value}
|
||||
></ha-bar>
|
||||
</div>
|
||||
</ha-settings-row>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-bar {
|
||||
--ha-bar-primary-color: var(--hassio-bar-ok-color, var(--success-color));
|
||||
}
|
||||
.target-warning {
|
||||
--ha-bar-primary-color: var(
|
||||
--hassio-bar-warning-color,
|
||||
var(--warning-color)
|
||||
);
|
||||
}
|
||||
.target-critical {
|
||||
--ha-bar-primary-color: var(
|
||||
--hassio-bar-critical-color,
|
||||
var(--error-color)
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 48px;
|
||||
padding-right: 4px;
|
||||
padding-inline-start: initial;
|
||||
padding-inline-end: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"supervisor-metric": SupervisorMetric;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/search-input";
|
||||
import type { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-addons")
|
||||
class HassioAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
suffix
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.supervisor.localize("dashboard.search_apps")}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`<h1>${this.supervisor.localize("dashboard.apps")}</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_apps")}
|
||||
</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.app_stopped")
|
||||
: addon.update_available!
|
||||
? this.supervisor.localize(
|
||||
"dashboard.app_new_version"
|
||||
)
|
||||
: this.supervisor.localize("dashboard.app_running")}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
105
|
||||
) && addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getAddons = memoizeOne(
|
||||
(addons: HassioAddonInfo[], filter?: string) => {
|
||||
if (filter) {
|
||||
addons = addons.filter((addon) => {
|
||||
const lowerCaseFilter = filter.toLowerCase();
|
||||
return (
|
||||
addon.name.toLowerCase().includes(lowerCaseFilter) ||
|
||||
addon.description.toLowerCase().includes(lowerCaseFilter) ||
|
||||
addon.slug.toLowerCase().includes(lowerCaseFilter)
|
||||
);
|
||||
});
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
direction: ltr;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
.content {
|
||||
margin-bottom: 72px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _addonTapped(ev: any): void {
|
||||
navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`);
|
||||
}
|
||||
|
||||
private _openStore(): void {
|
||||
navigate("/hassio/store");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addons": HassioAddons;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-fab";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
firstUpdated() {
|
||||
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
import("./hassio-update");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.header=${this.supervisor.localize("panel.apps")}
|
||||
>
|
||||
<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.apps"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
`
|
||||
: ""}
|
||||
<hassio-addons
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-addons>
|
||||
</div>
|
||||
|
||||
<a href="/hassio/store" slot="fab">
|
||||
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiStorePlus}
|
||||
></ha-svg-icon> </ha-fab
|
||||
></a>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleCheckUpdates() {
|
||||
try {
|
||||
await reloadHassioAddons(this.hass);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-fab.non-tabs {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right));
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom));
|
||||
inset-inline-end: calc(16px + var(--safe-area-inset-right));
|
||||
inset-inline-start: initial;
|
||||
z-index: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-dashboard": HassioDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import type { HassioHassOSInfo } from "../../../src/data/hassio/host";
|
||||
import type {
|
||||
HassioHomeAssistantInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
const computeVersion = (key: string, version: string): string =>
|
||||
key === "os" ? version : `${key}-${version}`;
|
||||
|
||||
@customElement("hassio-update")
|
||||
export class HassioUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
private _pendingUpdates = memoizeOne(
|
||||
(supervisor: Supervisor): number =>
|
||||
Object.keys(supervisor).filter(
|
||||
(value) => supervisor[value].update_available
|
||||
).length
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const updatesAvailable = this._pendingUpdates(this.supervisor);
|
||||
if (!updatesAvailable) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
${this.supervisor.localize("common.update_available", {
|
||||
count: updatesAvailable,
|
||||
})}
|
||||
🎉
|
||||
</h1>
|
||||
<div class="card-group">
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
"core",
|
||||
this.supervisor.core
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
"supervisor",
|
||||
this.supervisor.supervisor
|
||||
)}
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
"os",
|
||||
this.supervisor.os
|
||||
)
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUpdateCard(
|
||||
name: string,
|
||||
key: string,
|
||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
|
||||
) {
|
||||
if (!object.update_available) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="update-heading">${name}</div>
|
||||
<ha-settings-row two-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${computeVersion(key, object.version!)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row two-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${computeVersion(key, object.version_latest!)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" href="/hassio/update-available/${key}">
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.icon {
|
||||
--mdc-icon-size: 48px;
|
||||
float: right;
|
||||
margin: 0 0 2px 10px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.update-heading {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-content {
|
||||
height: calc(100% - 47px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-update": HassioUpdate;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { changeMountOptions } from "../../../../src/data/supervisor/mounts";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location";
|
||||
|
||||
const SCHEMA = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "default_backup_mount",
|
||||
required: true,
|
||||
selector: { backup_location: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
@customElement("dialog-hassio-backup-location")
|
||||
class HassioBackupLocationDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupLocationDialogParams;
|
||||
|
||||
@state() private _data?: { default_backup_mount: string | null };
|
||||
|
||||
@state() private _waiting?: boolean;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: HassioBackupLocationDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._waiting = undefined;
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this._dialogParams.supervisor.localize(
|
||||
"dialog.backup_location.title"
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${SCHEMA()}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
// @ts-ignore
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this._dialogParams!.supervisor.localize(
|
||||
`dialog.backup_location.options.${schema.name}.name`
|
||||
) || schema.name;
|
||||
|
||||
private _computeHelperCallback = (
|
||||
// @ts-ignore
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string =>
|
||||
this._dialogParams!.supervisor.localize(
|
||||
`dialog.backup_location.options.${schema.name}.description`
|
||||
);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const newLocation = ev.detail.value.default_backup_mount;
|
||||
this._data = {
|
||||
default_backup_mount: newLocation === "/backup" ? null : newLocation,
|
||||
};
|
||||
}
|
||||
|
||||
private async _changeMount() {
|
||||
if (!this._data) {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._waiting = true;
|
||||
try {
|
||||
await changeMountOptions(this.hass, this._data);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._waiting = false;
|
||||
return;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.delete-btn {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup-location": HassioBackupLocationDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-upload-backup";
|
||||
import type { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload";
|
||||
|
||||
@customElement("dialog-hassio-backup-upload")
|
||||
export class DialogHassioBackupUpload
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupUploadDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupUploadDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: HassioBackupUploadDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._dialogParams && !this._dialogParams.onboarding) {
|
||||
if (this._dialogParams.reloadBackup) {
|
||||
this._dialogParams.reloadBackup();
|
||||
}
|
||||
}
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title"
|
||||
>${this.hass?.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
) || "Upload backup"}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass?.localize("ui.common.close") || "Close"}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
dialogInitialFocus
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._dialogParams?.showBackup(backup.slug);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup-upload": DialogHassioBackupUpload;
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
|
||||
import { slugify } from "../../../../src/common/string/slugify";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-dialog-header";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import type { HassioBackupDetail } from "../../../../src/data/hassio/backup";
|
||||
import {
|
||||
fetchHassioBackupInfo,
|
||||
removeBackup,
|
||||
restoreBackup,
|
||||
} from "../../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _backup?: HassioBackupDetail;
|
||||
|
||||
@state() private _dialogParams?: HassioBackupDialogParams;
|
||||
|
||||
@state() private _restoringBackup = false;
|
||||
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(dialogParams: HassioBackupDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._backup = undefined;
|
||||
this._dialogParams = undefined;
|
||||
this._restoringBackup = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
.disableCancelAction=${!this._error}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${this._backup.name}
|
||||
>${this._backup.name}</span
|
||||
>
|
||||
${!this._dialogParams.onboarding && this._dialogParams.supervisor
|
||||
? html`<ha-button-menu
|
||||
slot="actionItems"
|
||||
fixed
|
||||
@action=${this._handleMenuAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"backup.more_actions"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
>${this._dialogParams.supervisor.localize(
|
||||
"backup.download_backup"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="error"
|
||||
>${this._dialogParams.supervisor.localize(
|
||||
"backup.delete_backup_title"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._restoringBackup
|
||||
? html`<div class="loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`
|
||||
: html`
|
||||
<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
`}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._downloadClicked();
|
||||
break;
|
||||
case 1:
|
||||
this._deleteClicked();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _restoreClicked() {
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._restoringBackup = true;
|
||||
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (supervisor !== undefined && supervisor.info.state !== "running") {
|
||||
await showAlertDialog(this, {
|
||||
title: supervisor.localize("backup.could_not_restore"),
|
||||
text: supervisor.localize("backup.restore_blocked_not_running", {
|
||||
state: supervisor.info.state,
|
||||
}),
|
||||
});
|
||||
this._restoringBackup = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
),
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restoreBackup(
|
||||
this.hass,
|
||||
this._backup!.type,
|
||||
this._backup!.slug,
|
||||
{ ...backupDetails, background: this._dialogParams?.onboarding },
|
||||
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
);
|
||||
|
||||
this._dialogParams?.onRestoring?.();
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteClicked() {
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (!supervisor) return;
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor!.localize("backup.confirm_delete_title"),
|
||||
text: supervisor!.localize("backup.confirm_delete_text"),
|
||||
confirmText: supervisor!.localize("backup.delete"),
|
||||
dismissText: supervisor!.localize("backup.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeBackup(this.hass!, this._backup!.slug);
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.body.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _downloadClicked() {
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (!supervisor) return;
|
||||
|
||||
let signedPath: { path: string };
|
||||
try {
|
||||
signedPath = await getSignedPath(
|
||||
this.hass!,
|
||||
`/api/hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/download`
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.href.includes("ui.nabu.casa")) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fileDownload(
|
||||
signedPath.path,
|
||||
`home_assistant_backup_${slugify(this._computeName)}.tar`
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.loading {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-backup": HassioBackupDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import {
|
||||
createHassioFullBackup,
|
||||
createHassioPartialBackup,
|
||||
} from "../../../../src/data/hassio/backup";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup";
|
||||
|
||||
@customElement("dialog-hassio-create-backup")
|
||||
class HassioCreateBackupDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioCreateBackupDialogParams;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _creatingBackup = false;
|
||||
|
||||
@query("supervisor-backup-content")
|
||||
private _backupContent!: SupervisorBackupContent;
|
||||
|
||||
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
this._creatingBackup = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
this._creatingBackup = false;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams.supervisor.localize("backup.create_backup")
|
||||
)}
|
||||
>
|
||||
${this._creatingBackup
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("common.close")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._creatingBackup}
|
||||
slot="primaryAction"
|
||||
@click=${this._createBackup}
|
||||
>
|
||||
${this._dialogParams.supervisor.localize("backup.create")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _createBackup(): Promise<void> {
|
||||
if (this._dialogParams!.supervisor.info.state !== "running") {
|
||||
showAlertDialog(this, {
|
||||
title: this._dialogParams!.supervisor.localize(
|
||||
"backup.could_not_create"
|
||||
),
|
||||
text: this._dialogParams!.supervisor.localize(
|
||||
"backup.create_blocked_not_running",
|
||||
{ state: this._dialogParams!.supervisor.info.state }
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._creatingBackup = true;
|
||||
|
||||
this._error = "";
|
||||
if (backupDetails.password && !backupDetails.password.length) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"backup.enter_password"
|
||||
);
|
||||
this._creatingBackup = false;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
backupDetails.password &&
|
||||
backupDetails.password !== backupDetails.confirm_password
|
||||
) {
|
||||
this._error = this._dialogParams!.supervisor.localize(
|
||||
"backup.passwords_not_matching"
|
||||
);
|
||||
this._creatingBackup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
delete backupDetails.confirm_password;
|
||||
|
||||
try {
|
||||
if (this._backupContent.backupType === "full") {
|
||||
await createHassioFullBackup(this.hass, backupDetails);
|
||||
} else {
|
||||
await createHassioPartialBackup(this.hass, backupDetails);
|
||||
}
|
||||
|
||||
this._dialogParams!.onCreate();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
this._creatingBackup = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-create-backup": HassioCreateBackupDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "./dialog-hassio-backup-upload";
|
||||
|
||||
export interface HassioBackupUploadDialogParams {
|
||||
showBackup: (slug: string) => void;
|
||||
reloadBackup?: () => Promise<void>;
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const showBackupUploadDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupUploadDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup-upload",
|
||||
dialogImport: () => import("./dialog-hassio-backup-upload"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupLocationDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioBackupLocationDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupLocationDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup-location",
|
||||
dialogImport: () => import("./dialog-hassio-backup-location"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
slug: string;
|
||||
onDelete?: () => void;
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioBackupDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-backup",
|
||||
dialogImport: () => import("./dialog-hassio-backup"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioCreateBackupDialogParams {
|
||||
supervisor: Supervisor;
|
||||
onCreate: () => void;
|
||||
}
|
||||
|
||||
export const showHassioCreateBackupDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioCreateBackupDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-create-backup",
|
||||
dialogImport: () => import("./dialog-hassio-create-backup"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-select";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import type { DatadiskList } from "../../../../src/data/hassio/host";
|
||||
import { listDatadisks, moveDatadisk } from "../../../../src/data/hassio/host";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk";
|
||||
|
||||
const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => {
|
||||
// Assume a speed of 30 MB/s.
|
||||
const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30;
|
||||
const rebootTime = (supervisor.host.startup_time * 4) / 60;
|
||||
return Math.ceil((moveTime + rebootTime) / 10) * 10;
|
||||
});
|
||||
|
||||
@customElement("dialog-hassio-datadisk")
|
||||
class HassioDatadiskDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private dialogParams?: HassioDatatiskDialogParams;
|
||||
|
||||
@state() private selectedDevice?: string;
|
||||
|
||||
@state() private devices?: DatadiskList["devices"];
|
||||
|
||||
@state() private moving = false;
|
||||
|
||||
public showDialog(params: HassioDatatiskDialogParams) {
|
||||
this.dialogParams = params;
|
||||
listDatadisks(this.hass).then((data) => {
|
||||
this.devices = data.devices;
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogParams = undefined;
|
||||
this.selectedDevice = undefined;
|
||||
this.devices = undefined;
|
||||
this.moving = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.moving
|
||||
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
|
||||
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
|
||||
@closed=${this.closeDialog}
|
||||
?hideActions=${this.moving}
|
||||
>
|
||||
${this.moving
|
||||
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.moving_desc"
|
||||
)}
|
||||
</p>`
|
||||
: html` ${this.devices?.length
|
||||
? html`
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.description",
|
||||
{
|
||||
current_path: this.dialogParams.supervisor.os.data_disk,
|
||||
time: calculateMoveTime(this.dialogParams.supervisor),
|
||||
}
|
||||
)}
|
||||
<br /><br />
|
||||
|
||||
<ha-select
|
||||
.label=${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.select_device"
|
||||
)}
|
||||
@selected=${this._selectDevice}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.devices.map(
|
||||
(device) =>
|
||||
html`<ha-list-item .value=${device}
|
||||
>${device}</ha-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: this.devices === undefined
|
||||
? this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.loading_devices"
|
||||
)
|
||||
: this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.no_devices"
|
||||
)}
|
||||
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.cancel"
|
||||
)}
|
||||
</ha-button>
|
||||
|
||||
<ha-button
|
||||
.disabled=${!this.selectedDevice}
|
||||
slot="primaryAction"
|
||||
@click=${this._moveDatadisk}
|
||||
>
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.move"
|
||||
)}
|
||||
</ha-button>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectDevice(ev) {
|
||||
this.selectedDevice = ev.target.value;
|
||||
}
|
||||
|
||||
private async _moveDatadisk() {
|
||||
this.moving = true;
|
||||
try {
|
||||
await moveDatadisk(this.hass, this.selectedDevice!);
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.dialogParams!.supervisor.localize(
|
||||
"system.host.failed_to_move"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-datadisk": HassioDatadiskDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioDatatiskDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showHassioDatadiskDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioDatatiskDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-datadisk",
|
||||
dialogImport: () => import("./dialog-hassio-datadisk"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/search-input";
|
||||
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||
|
||||
const _filterDevices = memoizeOne(
|
||||
(hardware: HassioHardwareInfo, filter: string, language: string) =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
device.by_id?.toLowerCase().includes(filter) ||
|
||||
device.name.toLowerCase().includes(filter) ||
|
||||
device.dev_path.toLocaleLowerCase().includes(filter) ||
|
||||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
@customElement("dialog-hassio-hardware")
|
||||
class HassioHardwareDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: HassioHardwareDialogParams;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
public showDialog(dialogParams: HassioHardwareDialogParams) {
|
||||
this._dialogParams = dialogParams;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const devices = _filterDevices(
|
||||
this._dialogParams.hardware,
|
||||
(this._filter || "").toLowerCase(),
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this._dialogParams.supervisor.localize(
|
||||
"dialog.hardware.title"
|
||||
)}
|
||||
>
|
||||
<div class="header" slot="heading">
|
||||
<h2>
|
||||
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
|
||||
</h2>
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
></ha-icon-button>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this._dialogParams.supervisor.localize(
|
||||
"dialog.hardware.search"
|
||||
)}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
|
||||
${devices.map(
|
||||
(device) =>
|
||||
html`<ha-expansion-panel
|
||||
.header=${device.name}
|
||||
.secondary=${device.by_id || undefined}
|
||||
outlined
|
||||
>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.subsystem"
|
||||
)}:
|
||||
</span>
|
||||
<span>${device.subsystem}</span>
|
||||
</div>
|
||||
<div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.device_path"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.dev_path}</code>
|
||||
</div>
|
||||
${device.by_id
|
||||
? html` <div class="device-property">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.id"
|
||||
)}:
|
||||
</span>
|
||||
<code>${device.by_id}</code>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="attributes">
|
||||
<span>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.hardware.attributes"
|
||||
)}:
|
||||
</span>
|
||||
<pre>${dump(device.attributes, { indent: 2 })}</pre>
|
||||
</div>
|
||||
</ha-expansion-panel>`
|
||||
)}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
inset-inline-end: 16px;
|
||||
inset-inline-start: initial;
|
||||
top: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
h2 {
|
||||
margin: 18px 42px 0 18px;
|
||||
margin-inline-start: 18px;
|
||||
margin-inline-end: 42px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 4px 0;
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
code {
|
||||
font-size: var(--ha-font-size-s);
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
search-input {
|
||||
margin: 8px 16px 0;
|
||||
display: block;
|
||||
}
|
||||
.device-property {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.attributes {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-hardware": HassioHardwareDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioHardwareDialogParams {
|
||||
supervisor: Supervisor;
|
||||
hardware: HassioHardwareInfo;
|
||||
}
|
||||
|
||||
export const showHassioHardwareDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioHardwareDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-hardware",
|
||||
dialogImport: () => import("./dialog-hassio-hardware"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { hassioStyle } from "../../resources/hassio-style";
|
||||
import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
|
||||
|
||||
@customElement("dialog-hassio-markdown")
|
||||
class HassioMarkdownDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property() public title!: string;
|
||||
|
||||
@property() public content!: string;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
public showDialog(params: HassioMarkdownDialogParams) {
|
||||
this.title = params.title;
|
||||
this.content = params.content;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this.title)}
|
||||
hideactions
|
||||
>
|
||||
<ha-markdown
|
||||
.content=${this.content || ""}
|
||||
dialogInitialFocus
|
||||
></ha-markdown>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
hassioStyle,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-markdown {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-markdown": HassioMarkdownDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
|
||||
export interface HassioMarkdownDialogParams {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const showHassioMarkdownDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioMarkdownDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-markdown",
|
||||
dialogImport: () => import("./dialog-hassio-markdown"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,647 +0,0 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-list";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
import "../../../../src/components/ha-password-field";
|
||||
import "../../../../src/components/ha-radio";
|
||||
import "../../../../src/components/ha-tab-group";
|
||||
import "../../../../src/components/ha-tab-group-tab";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import type {
|
||||
AccessPoints,
|
||||
NetworkInterface,
|
||||
WifiConfiguration,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import {
|
||||
accesspointScan,
|
||||
updateNetworkInterface,
|
||||
} from "../../../../src/data/hassio/network";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@customElement("dialog-hassio-network")
|
||||
export class DialogHassioNetwork
|
||||
extends LitElement
|
||||
implements HassDialog<HassioNetworkDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _accessPoints?: AccessPoints;
|
||||
|
||||
@state() private _curTabIndex = 0;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _interface?: NetworkInterface;
|
||||
|
||||
@state() private _interfaces!: NetworkInterface[];
|
||||
|
||||
@state() private _params?: HassioNetworkDialogParams;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _scanning = false;
|
||||
|
||||
@state() private _wifiConfiguration?: WifiConfiguration;
|
||||
|
||||
public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._dirty = false;
|
||||
this._curTabIndex = 0;
|
||||
this.supervisor = params.supervisor;
|
||||
this._interfaces = params.supervisor.network.interfaces.sort((a, b) =>
|
||||
a.primary > b.primary ? -1 : 1
|
||||
);
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
this._processing = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._interface) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.supervisor.localize("dialog.network.title")}
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this.supervisor.localize("dialog.network.title")}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
${this._interfaces.length > 1
|
||||
? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated}
|
||||
>${this._interfaces.map(
|
||||
(device, index) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.id=${device.interface}
|
||||
.panel=${index.toString()}
|
||||
.active=${this._curTabIndex === index}
|
||||
>
|
||||
${device.interface}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: ""}
|
||||
</div>
|
||||
${cache(this._renderTab())}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTab() {
|
||||
return html` <div class="form container">
|
||||
${IP_VERSIONS.map((version) =>
|
||||
this._interface![version] ? this._renderIPConfiguration(version) : ""
|
||||
)}
|
||||
${this._interface?.type === "wireless"
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
.header=${this.supervisor.localize("dialog.network.wifi")}
|
||||
outlined
|
||||
>
|
||||
${this._interface?.wifi?.ssid
|
||||
? html`<p>
|
||||
${this.supervisor.localize(
|
||||
"dialog.network.connected_to",
|
||||
{ ssid: this._interface?.wifi?.ssid }
|
||||
)}
|
||||
</p>`
|
||||
: ""}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
class="scan"
|
||||
@click=${this._scanForAP}
|
||||
.disabled=${this._scanning}
|
||||
.loading=${this._scanning}
|
||||
>
|
||||
${this.supervisor.localize("dialog.network.scan_ap")}
|
||||
</ha-button>
|
||||
${this._accessPoints &&
|
||||
this._accessPoints.accesspoints &&
|
||||
this._accessPoints.accesspoints.length !== 0
|
||||
? html`
|
||||
<ha-list>
|
||||
${this._accessPoints.accesspoints
|
||||
.filter((ap) => ap.ssid)
|
||||
.map(
|
||||
(ap) => html`
|
||||
<ha-list-item
|
||||
twoline
|
||||
@click=${this._selectAP}
|
||||
.activated=${ap.ssid ===
|
||||
this._wifiConfiguration?.ssid}
|
||||
.ap=${ap}
|
||||
>
|
||||
<span>${ap.ssid}</span>
|
||||
<span slot="secondary">
|
||||
${ap.mac} -
|
||||
${this.supervisor.localize(
|
||||
"dialog.network.signal_strength"
|
||||
)}:
|
||||
${ap.signal}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
`
|
||||
: ""}
|
||||
${this._wifiConfiguration
|
||||
? html`
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.open"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="open"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
undefined ||
|
||||
this._wifiConfiguration.auth === "open"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wep"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wep"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth === "wep"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wpa"
|
||||
)}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChangedAp}
|
||||
.ap=${this._wifiConfiguration}
|
||||
value="wpa-psk"
|
||||
name="auth"
|
||||
.checked=${this._wifiConfiguration.auth ===
|
||||
"wpa-psk"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-password-field
|
||||
class="flex-auto"
|
||||
id="psk"
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.network.wifi_password"
|
||||
)}
|
||||
version="wifi"
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-password-field>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: ""}
|
||||
${this._dirty
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize("dialog.network.warning")}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this.closeDialog} appearance="plain">
|
||||
${this.supervisor.localize("common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateNetwork}
|
||||
.disabled=${!this._dirty}
|
||||
.loading=${this._processing}
|
||||
>
|
||||
${this.supervisor.localize("common.save")}
|
||||
</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _selectAP(event) {
|
||||
this._wifiConfiguration = event.currentTarget.ap;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _scanForAP() {
|
||||
if (!this._interface) {
|
||||
return;
|
||||
}
|
||||
this._scanning = true;
|
||||
try {
|
||||
this._accessPoints = await accesspointScan(
|
||||
this.hass,
|
||||
this._interface.interface
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to scan for accesspoints",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
this._scanning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIPConfiguration(version: string) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
.header=${`IPv${version.charAt(version.length - 1)}`}
|
||||
outlined
|
||||
>
|
||||
<div class="radio-row">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.auto")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="auto"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "auto"}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.static")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="static"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "static"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize("dialog.network.disabled")}
|
||||
class="warning"
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
.version=${version}
|
||||
value="disabled"
|
||||
name="${version}method"
|
||||
.checked=${this._interface![version]?.method === "disabled"}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${this._interface![version].method === "static"
|
||||
? html`
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="address"
|
||||
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].address)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
.label=${this.supervisor.localize("dialog.network.gateway")}
|
||||
.version=${version}
|
||||
.value=${this._interface![version].gateway}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
.label=${this.supervisor.localize("dialog.network.dns_servers")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].nameservers)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toArray(data: string | string[]): string[] {
|
||||
if (Array.isArray(data)) {
|
||||
if (data && typeof data[0] === "string") {
|
||||
data = data[0];
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (typeof data === "string") {
|
||||
return data.replace(/ /g, "").split(",");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private _toString(data: string | string[]): string {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async _updateNetwork() {
|
||||
this._processing = true;
|
||||
let interfaceOptions: Partial<NetworkInterface> = {};
|
||||
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
interfaceOptions[version] = {
|
||||
method: this._interface![version]?.method || "auto",
|
||||
};
|
||||
if (this._interface![version]?.method === "static") {
|
||||
interfaceOptions[version] = {
|
||||
...interfaceOptions[version],
|
||||
address: this._toArray(this._interface![version]?.address),
|
||||
gateway: this._interface![version]?.gateway,
|
||||
nameservers: this._toArray(this._interface![version]?.nameservers),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (this._wifiConfiguration) {
|
||||
interfaceOptions = {
|
||||
...interfaceOptions,
|
||||
wifi: {
|
||||
ssid: this._wifiConfiguration.ssid,
|
||||
mode: this._wifiConfiguration.mode,
|
||||
auth: this._wifiConfiguration.auth || "open",
|
||||
},
|
||||
};
|
||||
if (interfaceOptions.wifi!.auth !== "open") {
|
||||
interfaceOptions.wifi = {
|
||||
...interfaceOptions.wifi,
|
||||
psk: this._wifiConfiguration.psk,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interfaceOptions.enabled =
|
||||
this._wifiConfiguration !== undefined ||
|
||||
interfaceOptions.ipv4?.method !== "disabled" ||
|
||||
interfaceOptions.ipv6?.method !== "disabled";
|
||||
|
||||
try {
|
||||
await updateNetworkInterface(
|
||||
this.hass,
|
||||
this._interface!.interface,
|
||||
interfaceOptions
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.network.failed_to_change"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
this._processing = false;
|
||||
return;
|
||||
}
|
||||
this._params?.loadData();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
|
||||
if (this._dirty) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("dialog.network.unsaved"),
|
||||
confirmText: this.supervisor.localize("common.yes"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
});
|
||||
if (!confirm) {
|
||||
this.requestUpdate("_interface");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._curTabIndex = Number(ev.detail.name);
|
||||
this._interface = { ...this._interfaces[this._curTabIndex] };
|
||||
}
|
||||
|
||||
private _handleRadioValueChanged(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as "disabled" | "auto" | "static";
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._interface[version]!.method === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
|
||||
this._interface[version]!.method = value;
|
||||
this.requestUpdate("_interface");
|
||||
}
|
||||
|
||||
private _handleRadioValueChangedAp(ev: CustomEvent): void {
|
||||
const value = (ev.target as any).value as string as
|
||||
| "open"
|
||||
| "wep"
|
||||
| "wpa-psk";
|
||||
this._wifiConfiguration!.auth = value;
|
||||
this._dirty = true;
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = source.id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._interface ||
|
||||
this._toString(this._interface[version]![id]) === this._toString(value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dirty = true;
|
||||
this._interface[version]![id] = value;
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
!this._wifiConfiguration ||
|
||||
this._wifiConfiguration![id] === value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._wifiConfiguration![id] = value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-content-position: static;
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
.container {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
|
||||
ha-button.scan {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 8px 4px;
|
||||
}
|
||||
.form {
|
||||
margin-bottom: 53px;
|
||||
}
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid
|
||||
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), 16px);
|
||||
background-color: var(--mdc-theme-surface, #fff);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
--primary-color: var(--error-color);
|
||||
}
|
||||
div.warning {
|
||||
margin: 12px 4px -12px;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
padding: 0 14px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 10px;
|
||||
}
|
||||
|
||||
ha-tab-group-tab {
|
||||
flex: 1;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-network": DialogHassioNetwork;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import "./dialog-hassio-network";
|
||||
|
||||
export interface HassioNetworkDialogParams {
|
||||
supervisor: Supervisor;
|
||||
loadData: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const showNetworkDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioNetworkDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-network",
|
||||
dialogImport: () => import("./dialog-hassio-network"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,255 +0,0 @@
|
||||
import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-button";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addHassioDockerRegistry,
|
||||
fetchHassioDockerRegistries,
|
||||
removeHassioDockerRegistry,
|
||||
} from "../../../../src/data/hassio/docker";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { RegistriesDialogParams } from "./show-dialog-registries";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "registry",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
required: true,
|
||||
selector: { text: { type: "password" } },
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("dialog-hassio-registries")
|
||||
class HassioRegistriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _registries?: {
|
||||
registry: string;
|
||||
username: string;
|
||||
}[];
|
||||
|
||||
@state() private _input: {
|
||||
registry?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
} = {};
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _addingRegistry = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._addingRegistry
|
||||
? this.supervisor.localize("dialog.registries.title_add")
|
||||
: this.supervisor.localize("dialog.registries.title_manage")
|
||||
)}
|
||||
>
|
||||
${this._addingRegistry
|
||||
? html`
|
||||
<ha-form
|
||||
.data=${this._input}
|
||||
.schema=${SCHEMA}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabel}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<div class="action">
|
||||
<ha-button
|
||||
?disabled=${Boolean(
|
||||
!this._input.registry ||
|
||||
!this._input.username ||
|
||||
!this._input.password
|
||||
)}
|
||||
@click=${this._addNewRegistry}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.supervisor.localize("dialog.registries.add_registry")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`
|
||||
: html`${this._registries?.length
|
||||
? this._registries.map(
|
||||
(entry) => html`
|
||||
<ha-settings-row class="registry">
|
||||
<span slot="heading"> ${entry.registry} </span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.username"
|
||||
)}:
|
||||
${entry.username}
|
||||
</span>
|
||||
<ha-icon-button
|
||||
.entry=${entry}
|
||||
.label=${this.supervisor.localize(
|
||||
"dialog.registries.remove"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
@click=${this._removeRegistry}
|
||||
></ha-icon-button>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.no_registries"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
<div class="action">
|
||||
<ha-button
|
||||
@click=${this._addRegistry}
|
||||
dialogInitialFocus
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.supervisor.localize(
|
||||
"dialog.registries.add_new_registry"
|
||||
)}
|
||||
</ha-button>
|
||||
</div> `}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.supervisor.localize(`dialog.registries.${schema.name}`);
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._input = ev.detail.value;
|
||||
}
|
||||
|
||||
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
|
||||
this._opened = true;
|
||||
this._input = {};
|
||||
this.supervisor = dialogParams.supervisor;
|
||||
await this._loadRegistries();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._addingRegistry = false;
|
||||
this._opened = false;
|
||||
this._input = {};
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.updateComplete.then(() =>
|
||||
(
|
||||
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
|
||||
)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadRegistries(): Promise<void> {
|
||||
const registries = await fetchHassioDockerRegistries(this.hass);
|
||||
this._registries = Object.keys(registries!.registries).map((key) => ({
|
||||
registry: key,
|
||||
username: registries.registries[key].username,
|
||||
}));
|
||||
}
|
||||
|
||||
private _addRegistry(): void {
|
||||
this._addingRegistry = true;
|
||||
}
|
||||
|
||||
private async _addNewRegistry(): Promise<void> {
|
||||
const data = {};
|
||||
data[this._input.registry!] = {
|
||||
username: this._input.username,
|
||||
password: this._input.password,
|
||||
};
|
||||
|
||||
try {
|
||||
await addHassioDockerRegistry(this.hass, data);
|
||||
await this._loadRegistries();
|
||||
this._addingRegistry = false;
|
||||
this._input = {};
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_add"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeRegistry(ev: Event): Promise<void> {
|
||||
const entry = (ev.currentTarget as any).entry;
|
||||
|
||||
try {
|
||||
await removeHassioDockerRegistry(this.hass, entry.registry);
|
||||
await this._loadRegistries();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
.registry {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.action {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--error-color);
|
||||
margin-right: -10px;
|
||||
margin-inline-end: -10px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-registries": HassioRegistriesDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import "./dialog-hassio-registries";
|
||||
|
||||
export interface RegistriesDialogParams {
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showRegistriesDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: RegistriesDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-registries",
|
||||
dialogImport: () => import("./dialog-hassio-registries"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,278 +0,0 @@
|
||||
import { mdiDelete, mdiDeleteOff, mdiPlus } from "@mdi/js";
|
||||
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 "../../../../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,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchStoreRepositories,
|
||||
removeStoreRepository,
|
||||
} 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-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?: HassioRepositoryDialogParams;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: HassioRepositoryDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._opened = true;
|
||||
await this._loadData();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialogParams = undefined;
|
||||
this._opened = false;
|
||||
this._error = "";
|
||||
}
|
||||
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core apps repository
|
||||
repo.slug !== "local" && // Locally managed apps
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
|
||||
repo.slug !== "5c53de3b" && // The ESPHome repository
|
||||
repo.slug !== "d5369777" // Music Assistant repository
|
||||
)
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
)
|
||||
);
|
||||
|
||||
private _filteredUsedRepositories = memoizeOne(
|
||||
(repos: HassioAddonRepository[], addons: HassioAddonInfo[]) =>
|
||||
repos
|
||||
.filter((repo) =>
|
||||
addons.some((addon) => addon.repository === repo.slug)
|
||||
)
|
||||
.map((repo) => repo.slug)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
const repositories = this._filteredRepositories(this._repositories);
|
||||
const usedRepositories = this._filteredUsedRepositories(
|
||||
repositories,
|
||||
this._dialogParams.supervisor.addon.addons
|
||||
);
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams!.supervisor.localize("dialog.repositories.title")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-md-list>
|
||||
${repositories.length
|
||||
? repositories.map(
|
||||
(repo) => html`
|
||||
<ha-md-list-item class="option">
|
||||
${repo.name}
|
||||
<div slot="supporting-text">
|
||||
<div>${repo.maintainer}</div>
|
||||
<div>${repo.url}</div>
|
||||
</div>
|
||||
<ha-tooltip
|
||||
.for="icon-button-${repo.slug}"
|
||||
class="delete"
|
||||
slot="end"
|
||||
>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
usedRepositories.includes(repo.slug)
|
||||
? "dialog.repositories.used"
|
||||
: "dialog.repositories.remove"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
<div .id="icon-button-${repo.slug}">
|
||||
<ha-icon-button
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
? mdiDeleteOff
|
||||
: mdiDelete}
|
||||
@click=${this._removeRepository}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)
|
||||
: html`<ha-md-list-item
|
||||
>${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.no_repositories"
|
||||
)}</ha-md-list-item
|
||||
>`}
|
||||
</ha-md-list>
|
||||
<div class="layout horizontal bottom">
|
||||
<ha-textfield
|
||||
class="flex-auto"
|
||||
id="repository_input"
|
||||
.value=${this._dialogParams!.url || ""}
|
||||
.label=${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
@keydown=${this._handleKeyAdd}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-button
|
||||
.loading=${this._processing}
|
||||
@click=${this._addRepository}
|
||||
appearance="filled"
|
||||
size="small"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this._dialogParams?.supervisor.localize("common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.option {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin-top: 4px;
|
||||
}
|
||||
ha-button {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
div.delete ha-icon-button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-md-list-item {
|
||||
position: relative;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.updateComplete.then(() =>
|
||||
(
|
||||
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
|
||||
)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyAdd(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
this._addRepository();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
try {
|
||||
this._repositories = await fetchStoreRepositories(this.hass);
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _addRepository() {
|
||||
const input = this._optionInput;
|
||||
if (!input || !input.value) {
|
||||
return;
|
||||
}
|
||||
this._processing = true;
|
||||
|
||||
try {
|
||||
await addStoreRepository(this.hass, input.value);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
|
||||
input.value = "";
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
this._processing = false;
|
||||
}
|
||||
|
||||
private async _removeRepository(ev: Event) {
|
||||
const slug = (ev.currentTarget as any).slug;
|
||||
try {
|
||||
await removeStoreRepository(this.hass, slug);
|
||||
await this._loadData();
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "store" });
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-repositories": HassioRepositoriesDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import "./dialog-hassio-repositories";
|
||||
|
||||
export interface HassioRepositoryDialogParams {
|
||||
supervisor: Supervisor;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const showRepositoriesDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioRepositoryDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-repositories",
|
||||
dialogImport: () => import("./dialog-hassio-repositories"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import { restartHassioAddon } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export const suggestAddonRestart = async (
|
||||
element: LitElement,
|
||||
hass: HomeAssistant,
|
||||
supervisor: Supervisor,
|
||||
addon: HassioAddonDetails
|
||||
): Promise<void> => {
|
||||
const confirmed = await showConfirmationDialog(element, {
|
||||
title: supervisor.localize("dialog.restart_app.title", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: supervisor.localize("dialog.restart_app.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_app.restart"),
|
||||
dismissText: supervisor.localize("common.cancel"),
|
||||
});
|
||||
if (confirmed) {
|
||||
try {
|
||||
await restartHassioAddon(hass, addon.slug);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(element, {
|
||||
title: supervisor.localize("common.failed_to_restart_name", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
import { mdiClose, mdiPuzzle, mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import "../../../../src/components/ha-dialog-header";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-icon-next";
|
||||
import "../../../../src/components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
getConfigEntry,
|
||||
type ConfigEntry,
|
||||
} from "../../../../src/data/config_entries";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { brandsUrl } from "../../../../src/util/brands-url";
|
||||
import type { SystemManagedDialogParams } from "./show-dialog-system-managed";
|
||||
|
||||
@customElement("dialog-system-managed")
|
||||
class HassioSystemManagedDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _supervisor?: Supervisor;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _configEntry?: ConfigEntry;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: SystemManagedDialogParams
|
||||
): Promise<void> {
|
||||
this._addon = dialogParams.addon;
|
||||
this._supervisor = dialogParams.supervisor;
|
||||
this._open = true;
|
||||
this._loadConfigEntry();
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._addon = undefined;
|
||||
this._supervisor = undefined;
|
||||
this._configEntry = undefined;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._addon || !this._open || !this._supervisor) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const addonImage =
|
||||
atLeastVersion(this.hass.config.version, 0, 105) && this._addon.icon
|
||||
? `/api/hassio/addons/${this._addon.slug}/icon`
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title">${this._addon?.name}</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="icons">
|
||||
<ha-svg-icon
|
||||
class="primary"
|
||||
.path=${mdiHomeAssistant}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
${addonImage
|
||||
? html`<img src=${addonImage} alt=${this._addon.name} />`
|
||||
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
|
||||
</div>
|
||||
${this._supervisor.localize("app.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("app.system_managed.description")}
|
||||
${this._configEntry
|
||||
? html`
|
||||
<h3>
|
||||
${this._supervisor.localize("app.system_managed.managed_by")}:
|
||||
</h3>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href=${`/config/integrations/integration/${this._configEntry.domain}`}
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
class="integration-icon"
|
||||
alt=${this._configEntry.title}
|
||||
src=${brandsUrl({
|
||||
domain: this._configEntry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
${this._configEntry.title}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.visibility = "initial";
|
||||
}
|
||||
|
||||
private _onImageError(ev) {
|
||||
ev.target.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
private async _loadConfigEntry() {
|
||||
if (this._addon?.system_managed_config_entry) {
|
||||
try {
|
||||
const { config_entry } = await getConfigEntry(
|
||||
this.hass,
|
||||
this._addon.system_managed_config_entry
|
||||
);
|
||||
this._configEntry = config_entry;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
--mdc-icon-size: 48px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.icons img {
|
||||
width: 48px;
|
||||
}
|
||||
.icons .primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.integration-icon {
|
||||
width: 24px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 4px;
|
||||
--md-list-item-trailing-space: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-system-managed": HassioSystemManagedDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface SystemManagedDialogParams {
|
||||
addon: HassioAddonDetails;
|
||||
supervisor: Supervisor;
|
||||
}
|
||||
|
||||
export const showSystemManagedDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SystemManagedDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-system-managed",
|
||||
dialogImport: () => import("./dialog-system-managed"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
(function () {
|
||||
function loadES5(src) {
|
||||
var el = document.createElement("script");
|
||||
el.src = src;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (<%= modernRegex %>.test(navigator.userAgent)) {
|
||||
try {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
new Function("import('<%= entry %>')")();
|
||||
<% } %>
|
||||
} catch (err) {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
haFontFamilyBody,
|
||||
haFontSmoothing,
|
||||
haMozOsxFontSmoothing,
|
||||
} from "../../src/resources/theme/typography.globals";
|
||||
import "./hassio-main";
|
||||
|
||||
import("../../src/resources/append-ha-style");
|
||||
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.textContent = `
|
||||
body {
|
||||
font-family: ${haFontFamilyBody};
|
||||
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
|
||||
-webkit-font-smoothing: ${haFontSmoothing};
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #111111;
|
||||
color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../src/common/dom/fire_event";
|
||||
import { mainWindow } from "../../src/common/dom/get_main_window";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import "./hassio-router";
|
||||
import { SupervisorBaseElement } from "./supervisor-base-element";
|
||||
|
||||
@customElement("hassio-main")
|
||||
export class HassioMain extends SupervisorBaseElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._applyTheme();
|
||||
|
||||
// Paulus - March 17, 2019
|
||||
// We went to a single hass-toggle-menu event in HA 0.90. However, the
|
||||
// supervisor UI can also run under older versions of Home Assistant.
|
||||
// So here we are going to translate toggle events into the appropriate
|
||||
// open and close events. These events are a no-op in newer versions of
|
||||
// Home Assistant.
|
||||
this.addEventListener("hass-toggle-menu", () => {
|
||||
fireEvent(
|
||||
(window.parent as any).customPanel,
|
||||
// @ts-ignore
|
||||
this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu"
|
||||
);
|
||||
});
|
||||
// Paulus - March 19, 2019
|
||||
// We changed the navigate event to fire directly on the window, as that's
|
||||
// where we are listening for it. However, the older panel_custom will
|
||||
// listen on this element for navigation events, so we need to forward them.
|
||||
|
||||
// Joakim - April 26, 2021
|
||||
// Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element
|
||||
mainWindow.addEventListener("location-changed", (ev) =>
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Paulus - May 17, 2021
|
||||
// Convert the <a> tags to native nav in Home Assistant < 2021.6
|
||||
document.body.addEventListener("click", (ev) => {
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
}
|
||||
});
|
||||
|
||||
// Forward haptic events to parent window.
|
||||
window.addEventListener("haptic", (ev) => {
|
||||
// @ts-ignore
|
||||
fireEvent(window.parent, ev.type, ev.detail, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Forward keydown events to the main window for quickbar access
|
||||
document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
|
||||
// Ignore if modifier keys are pressed
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
|
||||
bubbles: false,
|
||||
});
|
||||
});
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
if (oldHass.themes !== this.hass.themes) {
|
||||
this._applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hassio-router
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-router>
|
||||
`;
|
||||
}
|
||||
|
||||
private _applyTheme() {
|
||||
let themeName: string;
|
||||
let themeSettings: Partial<HomeAssistant["selectedTheme"]> | undefined;
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 0, 114)) {
|
||||
themeName =
|
||||
this.hass.selectedTheme?.theme ||
|
||||
(this.hass.themes.darkMode && this.hass.themes.default_dark_theme
|
||||
? this.hass.themes.default_dark_theme!
|
||||
: this.hass.themes.default_theme);
|
||||
|
||||
themeSettings = this.hass.selectedTheme;
|
||||
} else {
|
||||
themeName =
|
||||
(this.hass.selectedTheme as unknown as string) ||
|
||||
this.hass.themes.default_theme;
|
||||
}
|
||||
|
||||
applyThemesOnElement(
|
||||
this.parentElement,
|
||||
this.hass.themes,
|
||||
themeName,
|
||||
themeSettings,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-main": HassioMain;
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParamsObject,
|
||||
} from "../../src/common/url/search-params";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import "../../src/layouts/hass-error-screen";
|
||||
import type {
|
||||
ParamType,
|
||||
Redirect,
|
||||
Redirects,
|
||||
} from "../../src/panels/my/ha-panel-my";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
|
||||
export const REDIRECTS: Redirects = {
|
||||
supervisor: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_logs: {
|
||||
redirect: "/hassio/system",
|
||||
},
|
||||
supervisor_info: {
|
||||
redirect: "/hassio/system",
|
||||
},
|
||||
supervisor_snapshots: {
|
||||
redirect: "/hassio/backups",
|
||||
},
|
||||
supervisor_backups: {
|
||||
redirect: "/hassio/backups",
|
||||
},
|
||||
supervisor_store: {
|
||||
redirect: "/hassio/store",
|
||||
},
|
||||
supervisor_addons: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_addon: {
|
||||
redirect: "/hassio/addon",
|
||||
params: {
|
||||
addon: "string",
|
||||
},
|
||||
optional_params: {
|
||||
repository_url: "url",
|
||||
},
|
||||
},
|
||||
supervisor_ingress: {
|
||||
redirect: "/hassio/ingress",
|
||||
params: {
|
||||
addon: "string",
|
||||
},
|
||||
},
|
||||
supervisor_add_addon_repository: {
|
||||
redirect: "/hassio/store",
|
||||
params: {
|
||||
repository_url: "url",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("hassio-my-redirect")
|
||||
class HassioMyRedirect extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() public _error?: TemplateResult | string;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const path = this.route.path.substr(1);
|
||||
const redirect = REDIRECTS[path];
|
||||
|
||||
if (!redirect) {
|
||||
this._error = this.supervisor.localize("my.not_supported", {
|
||||
link: html`<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://my.home-assistant.io/faq.html#supported-pages"
|
||||
>
|
||||
${this.supervisor.localize("my.faq_link")}
|
||||
</a>`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let url: string;
|
||||
try {
|
||||
url = this._createRedirectUrl(redirect);
|
||||
} catch (_err: any) {
|
||||
this._error = this.supervisor.localize("my.error");
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(url, { replace: true });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<hass-error-screen
|
||||
.error=${this._error}
|
||||
></hass-error-screen>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _createRedirectUrl(redirect: Redirect): string {
|
||||
const params = this._createRedirectParams(redirect);
|
||||
return `${redirect.redirect}${params}`;
|
||||
}
|
||||
|
||||
private _createRedirectParams(redirect: Redirect): string {
|
||||
const params = extractSearchParamsObject();
|
||||
if (!redirect.params && !Object.keys(params).length) {
|
||||
return "";
|
||||
}
|
||||
const resultParams = {};
|
||||
Object.entries(redirect.params || {}).forEach(([key, type]) => {
|
||||
if (!params[key] || !this._checkParamType(type, params[key])) {
|
||||
throw Error();
|
||||
}
|
||||
resultParams[key] = params[key];
|
||||
});
|
||||
Object.entries(redirect.optional_params || {}).forEach(([key, type]) => {
|
||||
if (params[key]) {
|
||||
if (!this._checkParamType(type, params[key])) {
|
||||
throw Error();
|
||||
}
|
||||
resultParams[key] = params[key];
|
||||
}
|
||||
});
|
||||
return `?${createSearchParam(resultParams)}`;
|
||||
}
|
||||
|
||||
private _checkParamType(type: ParamType, value: string) {
|
||||
if (type === "string") {
|
||||
return true;
|
||||
}
|
||||
if (type === "url") {
|
||||
return value && value === sanitizeUrl(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-my-redirect": HassioMyRedirect;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import type { RouterOptions } from "../../src/layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../src/layouts/hass-router-page";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./dashboard/hassio-dashboard";
|
||||
|
||||
@customElement("hassio-panel-router")
|
||||
class HassioPanelRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
beforeRender: (page: string) =>
|
||||
page === "snapshots" ? "backups" : undefined,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-dashboard",
|
||||
},
|
||||
store: {
|
||||
tag: "hassio-addon-store",
|
||||
load: () => import("./addon-store/hassio-addon-store"),
|
||||
},
|
||||
backups: {
|
||||
tag: "hassio-backups",
|
||||
load: () => import("./backups/hassio-backups"),
|
||||
},
|
||||
system: {
|
||||
tag: "hassio-system",
|
||||
load: () => import("./system/hassio-system"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
el.hass = this.hass;
|
||||
el.supervisor = this.supervisor;
|
||||
el.route = this.route;
|
||||
el.narrow = this.narrow;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-panel-router": HassioPanelRouter;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import { supervisorCollection } from "../../src/data/supervisor/supervisor";
|
||||
import "../../src/layouts/hass-loading-screen";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
import "./hassio-panel-router";
|
||||
|
||||
@customElement("hassio-panel")
|
||||
class HassioPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(supervisorCollection).some(
|
||||
(collection) => !this.supervisor[collection]
|
||||
)
|
||||
) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
return html`
|
||||
<hassio-panel-router
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
></hassio-panel-router>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-panel": HassioPanel;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HassioPanelInfo } from "../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../src/data/supervisor/supervisor";
|
||||
import type { RouterOptions } from "../../src/layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../src/layouts/hass-router-page";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
// Don't codesplit it, that way the dashboard always loads fast.
|
||||
import "./hassio-panel";
|
||||
|
||||
@customElement("hassio-router")
|
||||
class HassioRouter extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public panel!: HassioPanelInfo;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
// Hass.io has a page with tabs, so we route all non-matching routes to it.
|
||||
defaultPage: "dashboard",
|
||||
beforeRender: (page: string) => {
|
||||
if (page === "snapshots") {
|
||||
return "backups";
|
||||
}
|
||||
if (page === "dashboard" && this.panel.config?.ingress) {
|
||||
return "ingress";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
showLoading: true,
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "hassio-panel",
|
||||
cache: true,
|
||||
},
|
||||
backups: "dashboard",
|
||||
store: "dashboard",
|
||||
system: "dashboard",
|
||||
"update-available": {
|
||||
tag: "update-available-dashboard",
|
||||
load: () => import("./update-available/update-available-dashboard"),
|
||||
},
|
||||
addon: {
|
||||
tag: "hassio-addon-dashboard",
|
||||
load: () => import("./addon-view/hassio-addon-dashboard"),
|
||||
},
|
||||
ingress: {
|
||||
tag: "hassio-ingress-view",
|
||||
load: () => import("./ingress-view/hassio-ingress-view"),
|
||||
},
|
||||
_my_redirect: {
|
||||
tag: "hassio-my-redirect",
|
||||
load: () => import("./hassio-my-redirect"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
protected updatePageEl(el) {
|
||||
// the tabs page does its own routing so needs full route.
|
||||
const hassioPanel = el.localName === "hassio-panel";
|
||||
const ingressPanel = el.localName === "hassio-ingress-view";
|
||||
const route = hassioPanel
|
||||
? this.route
|
||||
: ingressPanel && this.panel.config?.ingress
|
||||
? this._ingressRoute(this.panel.config?.ingress)
|
||||
: this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.route = route;
|
||||
el.supervisor = this.supervisor;
|
||||
|
||||
if (ingressPanel) {
|
||||
el.ingressPanel = Boolean(this.panel.config?.ingress);
|
||||
}
|
||||
}
|
||||
|
||||
private _ingressRoute = memoizeOne((ingress: string) => ({
|
||||
prefix: "/hassio/ingress",
|
||||
path: `/${ingress}`,
|
||||
}));
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-router": HassioRouter;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {
|
||||
mdiBackupRestore,
|
||||
mdiCogs,
|
||||
mdiPuzzle,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
atLeastVersion(hass.config.version, 2022, 5)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.apps"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? mdiPuzzle
|
||||
: mdiViewDashboard,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.backups",
|
||||
path: `/hassio/backups`,
|
||||
iconPath: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
translationKey: "panel.system",
|
||||
path: `/hassio/system`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
];
|
||||
@@ -1,377 +0,0 @@
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import { nextRender } from "../../../src/common/util/render-status";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
startHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
createHassioSession,
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
|
||||
@customElement("hassio-ingress-view")
|
||||
class HassioIngressView extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public ingressPanel = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@state() private _loadingMessage?: string;
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html`<hass-loading-screen
|
||||
.message=${this._loadingMessage}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const iframe = html`<iframe
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
>
|
||||
</iframe>`;
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.header=${this._addon.name}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${iframe}
|
||||
</hass-subpage>`;
|
||||
}
|
||||
|
||||
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
|
||||
? html`<div class="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@click=${this._toggleMenu}
|
||||
></ha-icon-button>
|
||||
<div class="main-title">${this._addon.name}</div>
|
||||
</div>
|
||||
${iframe}`
|
||||
: iframe}`;
|
||||
}
|
||||
|
||||
protected async firstUpdated(): Promise<void> {
|
||||
if (this.route.path === "") {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
let addonInfo: HassioAddonDetails;
|
||||
if (requestedAddon) {
|
||||
try {
|
||||
addonInfo = await fetchHassioAddonInfo(this.hass, requestedAddon);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: extractApiErrorMessage(err),
|
||||
title: requestedAddon,
|
||||
});
|
||||
await nextRender();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
return;
|
||||
}
|
||||
if (!addonInfo.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_app_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_app_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_app_info") ||
|
||||
"Unable to fetch app 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_app_not_installed") ||
|
||||
"The app 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_app_not_supported") ||
|
||||
"This app 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_app_not_running") ||
|
||||
"The app is not running. Do you want to start it now?",
|
||||
title: addon.name,
|
||||
confirmText:
|
||||
this.supervisor.localize("ingress.start_app") || "Start app",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app 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_app") ||
|
||||
"Error starting the app",
|
||||
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") {
|
||||
// App is starting up, wait for it to start
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app 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_app_not_ready") ||
|
||||
"The app seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
confirm: async () => {
|
||||
const addon = this._addon;
|
||||
this._addon = undefined;
|
||||
await Promise.all([
|
||||
this.updateComplete,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
}),
|
||||
]);
|
||||
this._addon = addon;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.header + iframe {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-l);
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin: var(--margin-title);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-ingress-view": HassioIngressView;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const hassioStyle = css`
|
||||
.content {
|
||||
margin: 8px;
|
||||
}
|
||||
h1,
|
||||
.description,
|
||||
.card-content {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
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: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.description {
|
||||
margin-top: 4px;
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.card-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: var(--ha-space-2);
|
||||
}
|
||||
@media screen and (min-width: 640px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.5fr));
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1020px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.333fr));
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1300px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
@@ -1,231 +0,0 @@
|
||||
import type { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../src/common/config/version";
|
||||
import { computeLocalize } from "../../src/common/translations/localize";
|
||||
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
|
||||
import type { HassioResponse } from "../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioHassOsInfo,
|
||||
fetchHassioHostInfo,
|
||||
} from "../../src/data/hassio/host";
|
||||
import { fetchNetworkInfo } from "../../src/data/hassio/network";
|
||||
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
} from "../../src/data/hassio/supervisor";
|
||||
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
|
||||
import type {
|
||||
Supervisor,
|
||||
SupervisorObject,
|
||||
SupervisorKeys,
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
getSupervisorEventCollection,
|
||||
supervisorCollection,
|
||||
cleanupSupervisorCollection,
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import type { HomeAssistant, Route } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
import {
|
||||
computeRTLDirection,
|
||||
setDirectionStyles,
|
||||
} from "../../src/common/util/compute_rtl";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"supervisor-update": Partial<Supervisor>;
|
||||
"supervisor-collection-refresh": { collection: SupervisorObject };
|
||||
}
|
||||
}
|
||||
|
||||
export class SupervisorBaseElement extends urlSyncMixin(
|
||||
ProvideHassLitMixin(LitElement)
|
||||
) {
|
||||
@property({ attribute: false }) public route?: Route;
|
||||
|
||||
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
|
||||
localize: () => "",
|
||||
};
|
||||
|
||||
@state() private _unsubs: Record<string, UnsubscribeFunc> = {};
|
||||
|
||||
@state() private _collections: Record<string, Collection<unknown>> = {};
|
||||
|
||||
@state() private _language = "en";
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
if (this.route?.prefix === "/hassio") {
|
||||
this._initSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
Object.keys(this._unsubs).forEach((unsub) => {
|
||||
this._unsubs[unsub]();
|
||||
delete this._unsubs[unsub];
|
||||
});
|
||||
Object.keys(this._collections).forEach((collection) => {
|
||||
cleanupSupervisorCollection(this.hass.connection, collection);
|
||||
});
|
||||
this._collections = {};
|
||||
this.removeEventListener(
|
||||
"supervisor-collection-refresh",
|
||||
this._handleSupervisorStoreRefreshEvent
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (this.route?.prefix === "/hassio") {
|
||||
this._initSupervisor();
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
const oldHass = changedProperties.get("hass") as
|
||||
| HomeAssistant
|
||||
| undefined;
|
||||
if (oldHass?.language !== this.hass.language) {
|
||||
this._language = this.hass.language;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("_language") || !this.hasUpdated) {
|
||||
this._initializeLocalize();
|
||||
this._applyDirection(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateSupervisor(update: Partial<Supervisor>): void {
|
||||
this.supervisor = { ...this.supervisor, ...update };
|
||||
}
|
||||
|
||||
private async _initializeLocalize() {
|
||||
const { language, data } = await getTranslation(null, this._language);
|
||||
|
||||
this._updateSupervisor({
|
||||
localize: await computeLocalize<SupervisorKeys>(
|
||||
this.constructor.prototype,
|
||||
language,
|
||||
{
|
||||
[language]: data,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleSupervisorStoreRefreshEvent(ev) {
|
||||
const collection = ev.detail.collection;
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
if (collection in this._collections) {
|
||||
this._collections[collection].refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.hass.callApi<HassioResponse<any>>(
|
||||
"GET",
|
||||
`hassio${supervisorCollection[collection]}`
|
||||
);
|
||||
this._updateSupervisor({ [collection]: response.data });
|
||||
}
|
||||
|
||||
private _subscribeCollection(collection: string) {
|
||||
if (this._unsubs[collection]) {
|
||||
this._unsubs[collection]();
|
||||
}
|
||||
try {
|
||||
this._unsubs[collection] = this._collections[collection].subscribe(
|
||||
(data) =>
|
||||
this._updateSupervisor({
|
||||
[collection]: data,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initSupervisor(): Promise<void> {
|
||||
this.addEventListener(
|
||||
"supervisor-collection-refresh",
|
||||
this._handleSupervisorStoreRefreshEvent
|
||||
);
|
||||
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
Object.keys(supervisorCollection).forEach((collection) => {
|
||||
if (collection in this._collections) {
|
||||
this._subscribeCollection(collection);
|
||||
this._collections[collection].refresh();
|
||||
} else {
|
||||
this._collections[collection] = getSupervisorEventCollection(
|
||||
this.hass.connection,
|
||||
collection,
|
||||
supervisorCollection[collection]
|
||||
);
|
||||
if (this._collections[collection].state) {
|
||||
// happens when the grace period of the collection unsubscribe has not passed yet
|
||||
this._updateSupervisor({
|
||||
[collection]: this._collections[collection].state,
|
||||
});
|
||||
}
|
||||
this._subscribeCollection(collection);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [
|
||||
addon,
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
store,
|
||||
] = await Promise.all([
|
||||
fetchHassioAddonsInfo(this.hass),
|
||||
fetchHassioSupervisorInfo(this.hass),
|
||||
fetchHassioHostInfo(this.hass),
|
||||
fetchHassioHomeAssistantInfo(this.hass),
|
||||
fetchHassioInfo(this.hass),
|
||||
fetchHassioHassOsInfo(this.hass),
|
||||
fetchNetworkInfo(this.hass),
|
||||
fetchHassioResolution(this.hass),
|
||||
fetchSupervisorStore(this.hass),
|
||||
]);
|
||||
|
||||
this._updateSupervisor({
|
||||
addon,
|
||||
supervisor,
|
||||
host,
|
||||
core,
|
||||
info,
|
||||
os,
|
||||
network,
|
||||
resolution,
|
||||
store,
|
||||
});
|
||||
|
||||
this.addEventListener("supervisor-update", (ev) =>
|
||||
this._updateSupervisor(ev.detail)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyDirection(hass: HomeAssistant) {
|
||||
const direction = computeRTLDirection(hass);
|
||||
setDirectionStyles(direction, this);
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import type { HassioStats } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { restartCore } from "../../../src/data/supervisor/core";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-core-info")
|
||||
class HassioCoreInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _metrics?: HassioStats;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("system.core.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("system.core.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-card header="Core" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
core-${this.supervisor.core.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
core-${this.supervisor.core.version_latest}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.core.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
href="/hassio/update-available/core"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
<div>
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
slot="primaryAction"
|
||||
variant="danger"
|
||||
@click=${this._coreRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Core",
|
||||
})}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", { name: "Core" })}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._metrics = await fetchHassioStats(this.hass, "core");
|
||||
}
|
||||
|
||||
private async _coreRestart(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("confirm.restart.title", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restartCore(this.hass);
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 124px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-core-info": HassioCoreInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
|
||||
import {
|
||||
changeHostOptions,
|
||||
configSyncOS,
|
||||
rebootHost,
|
||||
shutdownHost,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import type { NetworkInfo } from "../../../src/data/hassio/network";
|
||||
import { fetchNetworkInfo } from "../../../src/data/hassio/network";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../src/util/calculate";
|
||||
import "../components/supervisor-metric";
|
||||
import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk";
|
||||
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
|
||||
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-host-info")
|
||||
class HassioHostInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
const primaryIpAddress = this.supervisor.host.features.includes("network")
|
||||
? this._primaryIpAddress(this.supervisor.network!)
|
||||
: "";
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("system.host.used_space"),
|
||||
value: this._getUsedSpace(
|
||||
this.supervisor.host.disk_used,
|
||||
this.supervisor.host.disk_total
|
||||
),
|
||||
tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
|
||||
},
|
||||
];
|
||||
return html`
|
||||
<ha-card header="Host" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${this.supervisor.host.features.includes("hostname")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.hostname")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.hostname}
|
||||
</span>
|
||||
<ha-button
|
||||
@click=${this._changeHostnameClicked}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("system.host.change")}
|
||||
</ha-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.supervisor.host.features.includes("network")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.ip_address")}
|
||||
</span>
|
||||
<span slot="description"> ${primaryIpAddress} </span>
|
||||
<ha-button
|
||||
@click=${this._changeNetworkClicked}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("system.host.change")}
|
||||
</ha-button>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.operating_system")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.operating_system}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.os.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href="/hassio/update-available/os"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
${!this.supervisor.host.features.includes("haos")
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.docker_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.info.docker}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${this.supervisor.host.deployment
|
||||
? html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.deployment")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.deployment}
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
${this.supervisor.host.disk_life_time !== null
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.host.lifetime_used")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.host.disk_life_time} %
|
||||
</span>
|
||||
</ha-settings-row>`
|
||||
: ""}
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${this.supervisor.host.features.includes("reboot")
|
||||
? html`
|
||||
<ha-progress-button variant="danger" @click=${this._hostReboot}>
|
||||
${this.supervisor.localize("system.host.reboot_host")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
${this.supervisor.host.features.includes("shutdown")
|
||||
? html`
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
@click=${this._hostShutdown}
|
||||
>
|
||||
${this.supervisor.localize("system.host.shutdown_host")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
.label=${this.supervisor.localize("common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
.action=${"hardware"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.hardware")}
|
||||
</ha-list-item>
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? html`
|
||||
<ha-list-item
|
||||
.action=${"import_from_usb"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.import_from_usb")}
|
||||
</ha-list-item>
|
||||
${this.supervisor.host.features.includes("os_agent") &&
|
||||
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
|
||||
? html`
|
||||
<ha-list-item
|
||||
.action=${"move_datadisk"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.host.move_datadisk"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _getUsedSpace = memoizeOne((used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total))
|
||||
);
|
||||
|
||||
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
|
||||
if (!network_info || !network_info.interfaces) {
|
||||
return "";
|
||||
}
|
||||
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
|
||||
});
|
||||
|
||||
private async _handleMenuAction(ev) {
|
||||
switch ((ev.target as any).action) {
|
||||
case "hardware":
|
||||
await this._showHardware();
|
||||
break;
|
||||
case "import_from_usb":
|
||||
await this._importFromUSB();
|
||||
break;
|
||||
case "move_datadisk":
|
||||
await this._moveDatadisk();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDatadisk(): void {
|
||||
showHassioDatadiskDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
});
|
||||
}
|
||||
|
||||
private async _showHardware(): Promise<void> {
|
||||
let hardware;
|
||||
try {
|
||||
hardware = await fetchHassioHardwareInfo(this.hass);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.host.failed_to_get_hardware_list"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
|
||||
}
|
||||
|
||||
private async _hostReboot(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("system.host.reboot_host"),
|
||||
text: this.supervisor.localize("system.host.confirm_reboot"),
|
||||
confirmText: this.supervisor.localize("system.host.reboot_host"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await rebootHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_reboot"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _hostShutdown(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("system.host.shutdown_host"),
|
||||
text: this.supervisor.localize("system.host.confirm_shutdown"),
|
||||
confirmText: this.supervisor.localize("system.host.shutdown_host"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownHost(this.hass);
|
||||
} catch (err: any) {
|
||||
// Ignore connection errors, these are all expected
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_shutdown"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _changeNetworkClicked(): Promise<void> {
|
||||
showNetworkDialog(this, {
|
||||
supervisor: this.supervisor,
|
||||
loadData: () => this._loadData(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeHostnameClicked(): Promise<void> {
|
||||
const curHostname: string = this.supervisor.host.hostname;
|
||||
const hostname = await showPromptDialog(this, {
|
||||
title: this.supervisor.localize("system.host.change_hostname"),
|
||||
inputLabel: this.supervisor.localize("system.host.new_hostname"),
|
||||
inputType: "string",
|
||||
defaultValue: curHostname,
|
||||
confirmText: this.supervisor.localize("common.update"),
|
||||
});
|
||||
|
||||
if (hostname && hostname !== curHostname) {
|
||||
try {
|
||||
await changeHostOptions(this.hass, { hostname });
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "host",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _importFromUSB(): Promise<void> {
|
||||
try {
|
||||
await configSyncOS(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "host",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.host.failed_to_import_from_usb"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "network",
|
||||
});
|
||||
} else {
|
||||
const network = await fetchNetworkInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { network });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 124px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > span[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-menu-min-width: 200px;
|
||||
}
|
||||
ha-list-item ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-host-info": HassioHostInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HassioStats } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import type { SupervisorOptions } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
reloadSupervisor,
|
||||
restartSupervisor,
|
||||
setSupervisorOption,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta";
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/repairs/dialog-system-information";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import "../components/supervisor-metric";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _metrics?: HassioStats;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("system.supervisor.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("system.supervisor.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
return html`
|
||||
<ha-card header="Supervisor" outlined>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
supervisor-${this.supervisor.supervisor.version}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("common.newest_version")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
supervisor-${this.supervisor.supervisor.version_latest}
|
||||
</span>
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
|
||||
this.supervisor.supervisor.update_available
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href="/hassio/update-available/supervisor"
|
||||
>
|
||||
${this.supervisor.localize("common.show")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("system.supervisor.channel")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.supervisor.channel}
|
||||
</span>
|
||||
${this.supervisor.supervisor.channel === "beta"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.leave_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.leave_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisor.supervisor.channel === "stable"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisor.supervisor.supported
|
||||
? !atLeastVersion(this.hass.config.version, 2021, 4)
|
||||
? html` <ha-settings-row three-line>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics"
|
||||
)}
|
||||
</span>
|
||||
<div slot="description" class="diagnostics-description">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.share_diagnostics_description"
|
||||
)}
|
||||
<button
|
||||
class="link"
|
||||
.title=${this.supervisor.localize("common.show_more")}
|
||||
@click=${this._diagnosticsInformationDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</button>
|
||||
</div>
|
||||
<ha-switch
|
||||
haptic
|
||||
.checked=${this.supervisor.supervisor.diagnostics}
|
||||
@change=${this._toggleDiagnostics}
|
||||
></ha-switch>
|
||||
</ha-settings-row>`
|
||||
: ""
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_title"
|
||||
)}
|
||||
<ha-button
|
||||
slot="action"
|
||||
@click=${this._unsupportedDialog}
|
||||
variant="warning"
|
||||
size="small"
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</ha-button>
|
||||
</ha-alert>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.unhealthy_title"
|
||||
)}
|
||||
<ha-button
|
||||
variant="danger"
|
||||
size="small"
|
||||
slot="action"
|
||||
@click=${this._unhealthyDialog}
|
||||
>
|
||||
${this.supervisor.localize("common.learn_more")}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="metrics-block">
|
||||
${metrics.map(
|
||||
(metric) => html`
|
||||
<supervisor-metric
|
||||
.description=${metric.description}
|
||||
.value=${metric.value ?? 0}
|
||||
.tooltip=${metric.tooltip}
|
||||
></supervisor-metric>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@click=${this._supervisorReload}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.reload_supervisor"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize("system.supervisor.reload_supervisor")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._supervisorRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._metrics = await fetchHassioStats(this.hass, "supervisor");
|
||||
}
|
||||
|
||||
private async _toggleBeta(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
if (this.supervisor.supervisor.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => {
|
||||
await this._setChannel("beta");
|
||||
button.progress = false;
|
||||
},
|
||||
cancel: () => {
|
||||
button.progress = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this._setChannel("stable");
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setChannel(
|
||||
channel: SupervisorOptions["channel"]
|
||||
): Promise<void> {
|
||||
try {
|
||||
const data: Partial<SupervisorOptions> = {
|
||||
channel,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await this._reloadSupervisor();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.failed_to_set_option"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _supervisorReload(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
try {
|
||||
await this._reloadSupervisor();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _reloadSupervisor(): Promise<void> {
|
||||
await reloadSupervisor(this.hass);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "supervisor",
|
||||
});
|
||||
}
|
||||
|
||||
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("confirm.restart.title", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
button.progress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await restartSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _diagnosticsInformationDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.share_diagonstics_title"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"system.supervisor.share_diagonstics_description",
|
||||
{ line_break: html`<br /><br />` }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.unsupported_title"),
|
||||
text: html`${this.supervisor.localize(
|
||||
"system.supervisor.unsupported_description"
|
||||
)} <br /><br />
|
||||
<ul>
|
||||
${this.supervisor.resolution.unsupported.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNSUPPORTED_REASON_URL[reason] ||
|
||||
`/more-info/unsupported/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
`system.supervisor.unsupported_reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _unhealthyDialog(): Promise<void> {
|
||||
await showAlertDialog(this, {
|
||||
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
|
||||
text: html`${this.supervisor.localize(
|
||||
"system.supervisor.unhealthy_description"
|
||||
)} <br /><br />
|
||||
<ul>
|
||||
${this.supervisor.resolution.unhealthy.map(
|
||||
(reason) => html`
|
||||
<li>
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
UNHEALTHY_REASON_URL[reason] ||
|
||||
`/more-info/unhealthy/${reason}`
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
`system.supervisor.unhealthy_reason.${reason}`
|
||||
) || reason}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
diagnostics: !this.supervisor.supervisor?.diagnostics,
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"system.supervisor.failed_to_set_option"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
.card-actions {
|
||||
height: 48px;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 124px);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.metrics-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row[three-line] {
|
||||
height: 74px;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-supervisor-info": HassioSupervisorInfo;
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-select";
|
||||
import "../../../src/components/ha-list-item";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
interface LogProvider {
|
||||
key: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const logProviders: LogProvider[] = [
|
||||
{
|
||||
key: "supervisor",
|
||||
name: "Supervisor",
|
||||
},
|
||||
{
|
||||
key: "core",
|
||||
name: "Core",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
name: "Host",
|
||||
},
|
||||
{
|
||||
key: "dns",
|
||||
name: "DNS",
|
||||
},
|
||||
{
|
||||
key: "audio",
|
||||
name: "Audio",
|
||||
},
|
||||
{
|
||||
key: "multicast",
|
||||
name: "Multicast",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("hassio-supervisor-log")
|
||||
class HassioSupervisorLog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _selectedLogProvider = "supervisor";
|
||||
|
||||
@state() private _content?: string;
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
await this._loadData();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.supervisor.localize("system.log.log_provider")}
|
||||
@selected=${this._setLogProvider}
|
||||
.value=${this._selectedLogProvider}
|
||||
>
|
||||
${logProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item .value=${provider.key}>
|
||||
${provider.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="card-content" id="content">
|
||||
${this._content
|
||||
? html`<ha-ansi-to-html .content=${this._content}>
|
||||
</ha-ansi-to-html>`
|
||||
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._refresh}>
|
||||
${this.supervisor.localize("common.refresh")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLogProvider(ev): Promise<void> {
|
||||
const provider = ev.target.value;
|
||||
this._selectedLogProvider = provider;
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private async _refresh(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as any;
|
||||
button.progress = true;
|
||||
await this._loadData();
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
const response = await fetchHassioLogs(
|
||||
this.hass,
|
||||
this._selectedLogProvider
|
||||
);
|
||||
|
||||
this._content = await response.text();
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("system.log.get_logs", {
|
||||
provider: this._selectedLogProvider,
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
ha-select {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-supervisor-log": HassioSupervisorLog;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./hassio-core-info";
|
||||
import "./hassio-host-info";
|
||||
import "./hassio-supervisor-info";
|
||||
import "./hassio-supervisor-log";
|
||||
|
||||
@customElement("hassio-system")
|
||||
class HassioSystem extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult | undefined {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<hassio-core-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-core-info>
|
||||
<hassio-supervisor-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-supervisor-info>
|
||||
<hassio-host-info
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-host-info>
|
||||
</div>
|
||||
<hassio-supervisor-log
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-supervisor-log>
|
||||
</div>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
.content {
|
||||
margin: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.title {
|
||||
margin-top: 24px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 2em;
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
hassio-supervisor-log {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-system": HassioSystem;
|
||||
}
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
type CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-spinner";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-md-list";
|
||||
import "../../../src/components/ha-md-list-item";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import type { HaSwitch } from "../../../src/components/ha-switch";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
updateHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchHassioHomeAssistantInfo,
|
||||
fetchHassioSupervisorInfo,
|
||||
updateSupervisor,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"update-complete": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const SUPERVISOR_UPDATE_NAMES = {
|
||||
core: "Home Assistant Core",
|
||||
os: "Home Assistant Operating System",
|
||||
supervisor: "Home Assistant Supervisor",
|
||||
};
|
||||
|
||||
type UpdateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
entry: UpdateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
if (entry === "addon") {
|
||||
return undefined;
|
||||
}
|
||||
if (entry === "core") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: version.includes("b")
|
||||
? "https://next.home-assistant.io/latest-release-notes/"
|
||||
: "https://www.home-assistant.io/latest-release-notes/";
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
|
||||
}
|
||||
if (entry === "supervisor") {
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/supervisor/commits/main"
|
||||
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@customElement("update-available-card")
|
||||
class UpdateAvailableCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public addonSlug?: string;
|
||||
|
||||
@state() private _updateType?: UpdateType;
|
||||
|
||||
@state() private _changelogContent?: string;
|
||||
|
||||
@state() private _addonInfo?: HassioAddonDetails;
|
||||
|
||||
@state() private _updating = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._updateType ||
|
||||
(this._updateType === "addon" && !this._addonInfo)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const changelog = changelogUrl(this._updateType, this._version_latest);
|
||||
|
||||
const createBackupTexts = this._computeCreateBackupTexts();
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("update_available.update_name", {
|
||||
name: this._name,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._version === this._version_latest
|
||||
? html`<p>
|
||||
${this.supervisor.localize("update_available.no_update", {
|
||||
name: this._name,
|
||||
})}
|
||||
</p>`
|
||||
: !this._updating
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: nothing}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"update_available.description",
|
||||
{
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
${createBackupTexts
|
||||
? html`
|
||||
<hr />
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${createBackupTexts.title}
|
||||
</span>
|
||||
|
||||
${createBackupTexts.description
|
||||
? html`
|
||||
<span slot="supporting-text">
|
||||
${createBackupTexts.description}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id="create-backup"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: html`<ha-spinner
|
||||
aria-label="Updating"
|
||||
size="large"
|
||||
></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
})}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._version !== this._version_latest && !this._updating
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
? html`
|
||||
<ha-button
|
||||
href=${changelog}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<span></span>
|
||||
<ha-progress-button @click=${this._update}>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const pathPart = this.route?.path.substring(1, this.route.path.length);
|
||||
const updateType = ["core", "os", "supervisor"].includes(pathPart)
|
||||
? pathPart
|
||||
: "addon";
|
||||
this._updateType = updateType as UpdateType;
|
||||
|
||||
switch (updateType) {
|
||||
case "addon":
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
break;
|
||||
case "core":
|
||||
this._loadCoreData();
|
||||
break;
|
||||
case "supervisor":
|
||||
this._loadSupervisorData();
|
||||
break;
|
||||
case "os":
|
||||
this._loadOsData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
// App backup
|
||||
if (
|
||||
this._updateType === "addon" &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 2, 0)
|
||||
) {
|
||||
const version = this._version;
|
||||
return {
|
||||
title: this.supervisor.localize("update_available.create_backup.app"),
|
||||
description: this.supervisor.localize(
|
||||
"update_available.create_backup.app_description",
|
||||
{ version: version }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Old behavior
|
||||
if (this._updateType && ["core", "addon"].includes(this._updateType)) {
|
||||
return {
|
||||
title: this.supervisor.localize(
|
||||
"update_available.create_backup.generic"
|
||||
),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const createBackupSwitch = this.shadowRoot?.getElementById(
|
||||
"create-backup"
|
||||
) as HaSwitch;
|
||||
if (createBackupSwitch) {
|
||||
return createBackupSwitch.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version
|
||||
: this.supervisor[this._updateType]?.version || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _version_latest(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version_latest
|
||||
: this.supervisor[this._updateType]?.version_latest || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _name(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.name
|
||||
: SUPERVISOR_UPDATE_NAMES[this._updateType]
|
||||
: "";
|
||||
}
|
||||
|
||||
private async _loadAddonData() {
|
||||
try {
|
||||
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const addonStoreInfo =
|
||||
!this._addonInfo.detached && !this._addonInfo.available
|
||||
? this._addonStoreInfo(
|
||||
this._addonInfo.slug,
|
||||
this.supervisor.store.addons
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this._addonInfo.changelog) {
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
this.addonSlug!
|
||||
);
|
||||
this._changelogContent = extractChangelog(this._addonInfo, content);
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._addonInfo.available && addonStoreInfo) {
|
||||
if (
|
||||
!addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this._addonInfo.arch
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"app.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"app.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadSupervisorData() {
|
||||
try {
|
||||
const supervisor = await fetchHassioSupervisorInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { supervisor });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCoreData() {
|
||||
try {
|
||||
const core = await fetchHassioHomeAssistantInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { core });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadOsData() {
|
||||
try {
|
||||
const os = await fetchHassioHassOsInfo(this.hass);
|
||||
fireEvent(this, "supervisor-update", { os });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") {
|
||||
this._error = this.supervisor.localize("backup.backup_already_running");
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._updating = true;
|
||||
|
||||
try {
|
||||
if (this._updateType === "addon") {
|
||||
await updateHassioAddon(
|
||||
this.hass,
|
||||
this.addonSlug!,
|
||||
this._shouldCreateBackup
|
||||
);
|
||||
} else if (this._updateType === "core") {
|
||||
await updateCore(this.hass, this._shouldCreateBackup);
|
||||
} else if (this._updateType === "os") {
|
||||
await updateOS(this.hass);
|
||||
} else if (this._updateType === "supervisor") {
|
||||
await updateSupervisor(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._updating = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "update-complete");
|
||||
this._updating = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
margin: 16px 0 0 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-card": UpdateAvailableCard;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { goBack } from "../../../src/common/navigate";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../src/types";
|
||||
import "./update-available-card";
|
||||
|
||||
@customElement("update-available-dashboard")
|
||||
class UpdateAvailableDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
>
|
||||
<update-available-card
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
@update-complete=${this._updateComplete}
|
||||
></update-available-card>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
goBack();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
hass-subpage {
|
||||
--app-header-background-color: var(--primary-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
update-available-card {
|
||||
margin: auto;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-dashboard": UpdateAvailableDashboard;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import type { SupervisorArch } from "../../../src/data/supervisor/supervisor";
|
||||
|
||||
export const addonArchIsSupported = memoizeOne(
|
||||
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
|
||||
addon_archs.some((arch) => supported_archs.includes(arch))
|
||||
);
|
||||
|
||||
export const extractChangelog = (
|
||||
addon: HassioAddonDetails,
|
||||
content: string
|
||||
): string => {
|
||||
if (content.startsWith("# Changelog")) {
|
||||
content = content.substr(12, content.length);
|
||||
}
|
||||
if (
|
||||
content.includes(`# ${addon.version}`) &&
|
||||
content.includes(`# ${addon.version_latest}`)
|
||||
) {
|
||||
const newcontent = content.split(`# ${addon.version}`)[0];
|
||||
if (newcontent.includes(`# ${addon.version_latest}`)) {
|
||||
// Only change the content if the new version still exist
|
||||
// if the changelog does not have the newests version on top
|
||||
// this will not be true, and we don't modify the content
|
||||
content = newcontent;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
18
package.json
18
package.json
@@ -43,9 +43,9 @@
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.0",
|
||||
"@formatjs/intl-pluralrules": "6.2.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.1",
|
||||
"@formatjs/intl-pluralrules": "6.2.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.1",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -97,7 +97,7 @@
|
||||
"barcode-detector": "3.0.8",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.47.0",
|
||||
"core-js": "3.48.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -112,7 +112,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.0",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -156,7 +156,7 @@
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.0",
|
||||
"@rspack/core": "1.7.2",
|
||||
"@rspack/core": "1.7.3",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -207,15 +207,15 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.0",
|
||||
"prettier": "3.8.1",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.3",
|
||||
"tar": "7.5.6",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.53.0",
|
||||
"typescript-eslint": "8.53.1",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
16
script/core
16
script/core
@@ -53,23 +53,9 @@ logger:
|
||||
default: info
|
||||
logs:
|
||||
homeassistant.components.frontend: debug
|
||||
" >> "${WD}/config/configuration.yaml"
|
||||
|
||||
if [ -n "${HASSIO}" ]; then
|
||||
echo "
|
||||
# frontend:
|
||||
# development_repo: ${WD}
|
||||
|
||||
hassio:
|
||||
development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
else
|
||||
echo "
|
||||
frontend:
|
||||
development_repo: ${WD}
|
||||
|
||||
# hassio:
|
||||
# development_repo: ${WD}" >> "${WD}/config/configuration.yaml"
|
||||
fi
|
||||
development_repo: ${WD}" >> "${WD}/config/configuration.yaml" >> "${WD}/config/configuration.yaml"
|
||||
|
||||
if [ -n "${CODESPACES}" ]; then
|
||||
echo "
|
||||
|
||||
@@ -24,13 +24,16 @@ export const THEME_COLORS = new Set([
|
||||
"blue-grey",
|
||||
"black",
|
||||
"white",
|
||||
]);
|
||||
|
||||
const YAML_ONLY_THEMES_COLORS = new Set([
|
||||
"primary-text",
|
||||
"secondary-text",
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
export function computeCssColor(color: string): string {
|
||||
if (THEME_COLORS.has(color)) {
|
||||
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
|
||||
return `var(--${color}-color)`;
|
||||
}
|
||||
return color;
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_regist
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { WeatherEntity } from "../../data/weather";
|
||||
import { getWeatherUnit } from "../../data/weather";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import checkValidDate from "../datetime/check_valid_date";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../datetime/format_date_time";
|
||||
@@ -30,12 +30,34 @@ export const computeAttributeValueDisplay = (
|
||||
attribute: string,
|
||||
value?: any
|
||||
): string => {
|
||||
// Number value, return formatted number
|
||||
const parts = computeAttributeValueToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
config,
|
||||
entities,
|
||||
attribute,
|
||||
value
|
||||
);
|
||||
return parts.map((p) => p.value).join("");
|
||||
};
|
||||
|
||||
export const computeAttributeValueToParts = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
attribute: string,
|
||||
value?: any
|
||||
): ValuePart[] => {
|
||||
const attributeValue =
|
||||
value !== undefined ? value : stateObj.attributes[attribute];
|
||||
|
||||
// Null value, the state is unknown
|
||||
if (attributeValue === null || attributeValue === undefined) {
|
||||
return localize("state.default.unknown");
|
||||
return [{ type: "value", value: localize("state.default.unknown") }];
|
||||
}
|
||||
|
||||
// Number value, return formatted number
|
||||
@@ -58,11 +80,16 @@ export const computeAttributeValueDisplay = (
|
||||
unit = config.unit_system.temperature;
|
||||
}
|
||||
|
||||
if (unit) {
|
||||
return `${formattedValue}${blankBeforeUnit(unit, locale)}${unit}`;
|
||||
}
|
||||
const parts: ValuePart[] = [{ type: "value", value: formattedValue }];
|
||||
|
||||
return formattedValue;
|
||||
if (unit) {
|
||||
const literal = blankBeforeUnit(unit, locale);
|
||||
if (literal) {
|
||||
parts.push({ type: "literal", value: literal });
|
||||
}
|
||||
parts.push({ type: "unit", value: unit });
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Special handling in case this is a string with a known format
|
||||
@@ -73,14 +100,15 @@ export const computeAttributeValueDisplay = (
|
||||
if (isTimestamp(attributeValue)) {
|
||||
const date = new Date(attributeValue);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDateTimeWithSeconds(date, locale, config);
|
||||
const formattedDate = formatDateTimeWithSeconds(date, locale, config);
|
||||
return [{ type: "value", value: formattedDate }];
|
||||
}
|
||||
}
|
||||
|
||||
// Value was not a timestamp, so only do date formatting
|
||||
const date = new Date(attributeValue);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDate(date, locale, config);
|
||||
return [{ type: "value", value: formatDate(date, locale, config) }];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,11 +119,11 @@ export const computeAttributeValueDisplay = (
|
||||
attributeValue.some((val) => val instanceof Object)) ||
|
||||
(!Array.isArray(attributeValue) && attributeValue instanceof Object)
|
||||
) {
|
||||
return JSON.stringify(attributeValue);
|
||||
return [{ type: "value", value: JSON.stringify(attributeValue) }];
|
||||
}
|
||||
// If this is an array, try to determine the display value for each item
|
||||
if (Array.isArray(attributeValue)) {
|
||||
return attributeValue
|
||||
const formattedValue = attributeValue
|
||||
.map((item) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
@@ -108,6 +136,7 @@ export const computeAttributeValueDisplay = (
|
||||
)
|
||||
)
|
||||
.join(", ");
|
||||
return [{ type: "value", value: formattedValue }];
|
||||
}
|
||||
|
||||
// We've explored all known value handling, so now we'll try to find a
|
||||
@@ -120,7 +149,7 @@ export const computeAttributeValueDisplay = (
|
||||
| undefined;
|
||||
const translationKey = registryEntry?.translation_key;
|
||||
|
||||
return (
|
||||
const formattedValue =
|
||||
(translationKey &&
|
||||
localize(
|
||||
`component.${registryEntry.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}`
|
||||
@@ -132,8 +161,8 @@ export const computeAttributeValueDisplay = (
|
||||
localize(
|
||||
`component.${domain}.entity_component._.state_attributes.${attribute}.state.${attributeValue}`
|
||||
) ||
|
||||
attributeValue
|
||||
);
|
||||
attributeValue;
|
||||
return [{ type: "value", value: formattedValue }];
|
||||
};
|
||||
|
||||
export const computeAttributeNameDisplay = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import {
|
||||
computeEntityNameDisplay,
|
||||
type EntityNameItem,
|
||||
@@ -17,6 +17,11 @@ export type FormatEntityAttributeValueFunc = (
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => string;
|
||||
export type FormatEntityAttributeValueToPartsFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
value?: any
|
||||
) => ValuePart[];
|
||||
export type FormatEntityAttributeNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string
|
||||
@@ -42,13 +47,17 @@ export const computeFormatFunctions = async (
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } =
|
||||
await import("../entity/compute_state_display");
|
||||
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
||||
await import("../entity/compute_attribute_display");
|
||||
const {
|
||||
computeAttributeValueDisplay,
|
||||
computeAttributeValueToParts,
|
||||
computeAttributeNameDisplay,
|
||||
} = await import("../entity/compute_attribute_display");
|
||||
|
||||
return {
|
||||
formatEntityState: (stateObj, state) =>
|
||||
@@ -71,6 +80,16 @@ export const computeFormatFunctions = async (
|
||||
attribute,
|
||||
value
|
||||
),
|
||||
formatEntityAttributeValueToParts: (stateObj, attribute, value) =>
|
||||
computeAttributeValueToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
config,
|
||||
entities,
|
||||
attribute,
|
||||
value
|
||||
),
|
||||
formatEntityAttributeName: (stateObj, attribute) =>
|
||||
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||
formatEntityName: (stateObj, name, options) =>
|
||||
|
||||
19
src/common/util/media-progress.ts
Normal file
19
src/common/util/media-progress.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const startMediaProgressInterval = (
|
||||
interval: number | undefined,
|
||||
callback: () => void,
|
||||
intervalMs = 1000
|
||||
): number => {
|
||||
if (interval) {
|
||||
return interval;
|
||||
}
|
||||
return window.setInterval(callback, intervalMs);
|
||||
};
|
||||
|
||||
export const stopMediaProgressInterval = (
|
||||
interval: number | undefined
|
||||
): number | undefined => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
186
src/common/util/volume-slider.ts
Normal file
186
src/common/util/volume-slider.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { HaSlider } from "../../components/ha-slider";
|
||||
|
||||
interface VolumeSliderControllerOptions {
|
||||
getSlider: () => HaSlider | undefined;
|
||||
step: number;
|
||||
onSetVolume: (value: number) => void;
|
||||
onSetVolumeDebounced?: (value: number) => void;
|
||||
onValueUpdated?: (value: number) => void;
|
||||
}
|
||||
|
||||
export class VolumeSliderController {
|
||||
private _touchStartX = 0;
|
||||
|
||||
private _touchStartY = 0;
|
||||
|
||||
private _touchStartValue = 0;
|
||||
|
||||
private _touchDragging = false;
|
||||
|
||||
private _touchScrolling = false;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _lastValue = 0;
|
||||
|
||||
private _options: VolumeSliderControllerOptions;
|
||||
|
||||
constructor(options: VolumeSliderControllerOptions) {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public get isInteracting(): boolean {
|
||||
return this._touchDragging || this._dragging;
|
||||
}
|
||||
|
||||
public setStep(step: number): void {
|
||||
this._options.step = step;
|
||||
}
|
||||
|
||||
public handleInput = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = true;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolumeDebounced?.(value);
|
||||
};
|
||||
|
||||
public handleChange = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = false;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolume(value);
|
||||
};
|
||||
|
||||
public handleTouchStart = (ev: TouchEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const touch = ev.touches[0];
|
||||
this._touchStartX = touch.clientX;
|
||||
this._touchStartY = touch.clientY;
|
||||
this._touchStartValue = this._getSliderValue();
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._showTooltip();
|
||||
};
|
||||
|
||||
public handleTouchMove = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
return;
|
||||
}
|
||||
const touch = ev.touches[0];
|
||||
const deltaX = touch.clientX - this._touchStartX;
|
||||
const deltaY = touch.clientY - this._touchStartY;
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
if (!this._touchDragging) {
|
||||
if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) {
|
||||
this._touchScrolling = true;
|
||||
return;
|
||||
}
|
||||
if (absDeltaX > 8) {
|
||||
this._touchDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._touchDragging) {
|
||||
ev.preventDefault();
|
||||
const newValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
public handleTouchEnd = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
this._touchScrolling = false;
|
||||
this._hideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = ev.changedTouches[0];
|
||||
if (!this._touchDragging) {
|
||||
const tapValue = this._getVolumeFromTouch(touch.clientX);
|
||||
const delta =
|
||||
tapValue > this._touchStartValue
|
||||
? this._options.step
|
||||
: -this._options.step;
|
||||
const newValue = this._roundVolumeValue(this._touchStartValue + delta);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
} else {
|
||||
const finalValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(finalValue);
|
||||
this._options.onSetVolume(finalValue);
|
||||
}
|
||||
|
||||
this._touchDragging = false;
|
||||
this._dragging = false;
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleTouchCancel = (): void => {
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._dragging = false;
|
||||
this._updateValue(this._touchStartValue);
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleWheel = (ev: WheelEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const direction = ev.deltaY > 0 ? -1 : 1;
|
||||
const currentValue = this._getSliderValue();
|
||||
const newValue = this._roundVolumeValue(
|
||||
currentValue + direction * this._options.step
|
||||
);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
};
|
||||
|
||||
private _getVolumeFromTouch(clientX: number): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (!slider) {
|
||||
return 0;
|
||||
}
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
|
||||
const percentage = (x / rect.width) * 100;
|
||||
return this._roundVolumeValue(percentage);
|
||||
}
|
||||
|
||||
private _roundVolumeValue(value: number): number {
|
||||
return Math.min(
|
||||
Math.max(Math.round(value / this._options.step) * this._options.step, 0),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
private _getSliderValue(): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
return Number(slider.value);
|
||||
}
|
||||
return this._lastValue;
|
||||
}
|
||||
|
||||
private _updateValue(value: number): void {
|
||||
this._lastValue = value;
|
||||
this._options.onValueUpdated?.(value);
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
slider.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private _showTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.showTooltip?.();
|
||||
}
|
||||
|
||||
private _hideTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.hideTooltip?.();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
@@ -21,10 +20,6 @@ class HaAttributeValue extends LitElement {
|
||||
}
|
||||
const attributeValue = this.stateObj.attributes[this.attribute];
|
||||
|
||||
if (typeof attributeValue === "number" && this.hideUnit) {
|
||||
return formatNumber(attributeValue, this.hass.locale);
|
||||
}
|
||||
|
||||
if (typeof attributeValue === "string") {
|
||||
// URL handling
|
||||
if (attributeValue.startsWith("http")) {
|
||||
@@ -56,6 +51,14 @@ class HaAttributeValue extends LitElement {
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
}
|
||||
|
||||
if (this.hideUnit) {
|
||||
const parts = this.hass.formatEntityAttributeValueToParts(
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts.find((part) => part.type === "value")?.value;
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
import type { HaButton } from "./ha-button";
|
||||
import "./ha-menu";
|
||||
|
||||
@customElement("ha-button-menu")
|
||||
export class HaButtonMenu extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property() public corner: Corner = "BOTTOM_START";
|
||||
|
||||
@property({ attribute: "menu-corner" }) public menuCorner: MenuCorner =
|
||||
"START";
|
||||
|
||||
@property({ type: Number }) public x: number | null = null;
|
||||
|
||||
@property({ type: Number }) public y: number | null = null;
|
||||
|
||||
@property({ type: Boolean }) public multi = false;
|
||||
|
||||
@property({ type: Boolean }) public activatable = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public fixed = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false;
|
||||
|
||||
@query("ha-menu", true) private _menu?: Menu;
|
||||
|
||||
public get items() {
|
||||
return this._menu?.items;
|
||||
}
|
||||
|
||||
public get selected() {
|
||||
return this._menu?.selected;
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
if (this._menu?.open) {
|
||||
this._menu.focusItemAtIndex(0);
|
||||
} else {
|
||||
this._triggerButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div @click=${this._handleClick}>
|
||||
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||
</div>
|
||||
<ha-menu
|
||||
.corner=${this.corner}
|
||||
.menuCorner=${this.menuCorner}
|
||||
.fixed=${this.fixed}
|
||||
.multi=${this.multi}
|
||||
.activatable=${this.activatable}
|
||||
.y=${this.y}
|
||||
.x=${this.x}
|
||||
>
|
||||
<slot></slot>
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (mainWindow.document.dir === "rtl") {
|
||||
this.updateComplete.then(() => {
|
||||
this.querySelectorAll("ha-list-item").forEach((item) => {
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML =
|
||||
"span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}";
|
||||
item!.shadowRoot!.appendChild(style);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._menu!.anchor = this.noAnchor ? null : this;
|
||||
this._menu!.show();
|
||||
}
|
||||
|
||||
private get _triggerButton() {
|
||||
return this.querySelector(
|
||||
'ha-icon-button[slot="trigger"], ha-button[slot="trigger"]'
|
||||
) as HaIconButton | HaButton | null;
|
||||
}
|
||||
|
||||
private _setTriggerAria() {
|
||||
if (this._triggerButton) {
|
||||
this._triggerButton.ariaHasPopup = "menu";
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
::slotted([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-menu": HaButtonMenu;
|
||||
}
|
||||
}
|
||||
@@ -691,15 +691,10 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
private _getIconItems = async (): Promise<Completion[]> => {
|
||||
if (!this._iconList) {
|
||||
let iconList: {
|
||||
const iconList: {
|
||||
name: string;
|
||||
keywords: string[];
|
||||
}[];
|
||||
if (__SUPERVISOR__) {
|
||||
iconList = [];
|
||||
} else {
|
||||
iconList = (await import("../../build/mdi/iconList.json")).default;
|
||||
}
|
||||
}[] = (await import("../../build/mdi/iconList.json")).default;
|
||||
|
||||
this._iconList = iconList.map((icon) => ({
|
||||
type: "variable",
|
||||
|
||||
@@ -3,15 +3,14 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-textfield";
|
||||
import "../ha-md-button-menu";
|
||||
import "../ha-md-menu-item";
|
||||
import "../ha-textfield";
|
||||
|
||||
import type {
|
||||
HaFormElement,
|
||||
@@ -41,7 +40,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-button-menu") private _input?: HTMLElement;
|
||||
@query("ha-md-button-menu") private _input?: HTMLElement;
|
||||
|
||||
public focus(): void {
|
||||
if (this._input) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-md-button-menu";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./ha-md-menu-item";
|
||||
import "./ha-md-divider";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
@@ -39,10 +39,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
return html`
|
||||
${this.narrow
|
||||
? html` <!-- Collapsed representation for small screens -->
|
||||
<ha-md-button-menu
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
positioning="popover"
|
||||
>
|
||||
<ha-dropdown @wa-show=${this._handleIconOverflowMenuOpened}>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@@ -51,24 +48,17 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
|
||||
${this.items.map((item) =>
|
||||
item.divider
|
||||
? html`<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
: html`<ha-md-menu-item
|
||||
? html`<wa-divider></wa-divider>`
|
||||
: html`<ha-dropdown-item
|
||||
?disabled=${item.disabled}
|
||||
.clickAction=${item.action}
|
||||
class=${classMap({ warning: Boolean(item.warning) })}
|
||||
@click=${item.action}
|
||||
variant=${item.warning ? "danger" : "default"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class=${classMap({ warning: Boolean(item.warning) })}
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon slot="icon" .path=${item.path}></ha-svg-icon>
|
||||
${item.label}
|
||||
</ha-md-menu-item>`
|
||||
</ha-dropdown-item>`
|
||||
)}
|
||||
</ha-md-button-menu>`
|
||||
</ha-dropdown>`
|
||||
: html`
|
||||
<!-- Icon representation for big screens -->
|
||||
${this.items.map((item) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
@@ -9,9 +9,16 @@ import {
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getLabels, labelComboBoxKeys } from "../data/label/label_picker";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
type LabelComboBoxItem,
|
||||
} from "../data/label/label_picker";
|
||||
import {
|
||||
createLabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
@@ -24,11 +31,26 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import {
|
||||
type PickerComboBoxItem,
|
||||
DEFAULT_ROW_RENDERER_CONTENT,
|
||||
} from "./ha-picker-combo-box";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
export const renderLabelColorBadge = (color: string | undefined) =>
|
||||
html`<div
|
||||
style=${styleMap({
|
||||
backgroundColor: color ? computeCssColor(color) : undefined,
|
||||
borderRadius: "var(--ha-border-radius-md)",
|
||||
border: "1px solid var(--outline-color)",
|
||||
boxSizing: "border-box",
|
||||
width: "var(--ha-space-5)",
|
||||
height: "var(--ha-space-5)",
|
||||
})}
|
||||
></div>`;
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -106,6 +128,16 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _rowRenderer: RenderItemFunction<LabelComboBoxItem> = (item) =>
|
||||
html`<ha-combo-box-item type="button" compact>
|
||||
${DEFAULT_ROW_RENDERER_CONTENT(item)}
|
||||
${item.id !== ADD_NEW_ID
|
||||
? html`<div slot="trailing-supporting-text">
|
||||
${renderLabelColorBadge(item.color)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>`;
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () =>
|
||||
@@ -190,6 +222,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user