Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
2c440976aa use heading property 2021-09-30 09:31:27 -07:00
590 changed files with 13269 additions and 29769 deletions

View File

@@ -29,7 +29,6 @@
"__BUILD__": false, "__BUILD__": false,
"__VERSION__": false, "__VERSION__": false,
"__STATIC_PATH__": false, "__STATIC_PATH__": false,
"__SUPERVISOR__": false,
"Polymer": true "Polymer": true
}, },
"env": { "env": {
@@ -112,7 +111,8 @@
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"lit/attribute-value-entities": "off", "lit/attribute-value-entities": "off",
"lit/no-template-map": "off" "lit/no-template-map": "off",
"lit/no-template-arrow": "warn"
}, },
"plugins": ["disable", "unused-imports"], "plugins": ["disable", "unused-imports"],
"processor": "disable/disable" "processor": "disable/disable"

View File

@@ -30,7 +30,7 @@ jobs:
env: env:
CI: true CI: true
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-demos run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos
- name: Run eslint - name: Run eslint
run: yarn run lint:eslint run: yarn run lint:eslint
- name: Run tsc - name: Run tsc
@@ -53,8 +53,6 @@ jobs:
run: yarn install run: yarn install
env: env:
CI: true CI: true
- name: Build resources
run: ./node_modules/.bin/gulp build-translations build-locale-data
- name: Run Tests - name: Run Tests
run: yarn run test run: yarn run test
build: build:

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
# build # build
build build
build-translations/*
hass_frontend/* hass_frontend/*
dist dist

View File

@@ -1,4 +1,5 @@
build build
build-translations/*
translations/* translations/*
node_modules/* node_modules/*
hass_frontend/* hass_frontend/*

View File

@@ -1,12 +0,0 @@
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
--- a/mwc-icon-button-base.js
+++ b/mwc-icon-button-base.js
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
@touchend="${this.handleRippleDeactivate}"
@touchcancel="${this.handleRippleDeactivate}"
>${this.renderRipple()}
- <i class="material-icons">${this.icon}</i>
<span
><slot></slot
></span>

View File

@@ -35,7 +35,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()), __VERSION__: JSON.stringify(env.version()),
__DEMO__: false, __DEMO__: false,
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false, __BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/", __STATIC_PATH__: "/static/",
"process.env.NODE_ENV": JSON.stringify( "process.env.NODE_ENV": JSON.stringify(
@@ -165,7 +164,6 @@ module.exports.config = {
cast({ isProdBuild, latestBuild }) { cast({ isProdBuild, latestBuild }) {
const entry = { const entry = {
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"), launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
}; };
if (latestBuild) { if (latestBuild) {
@@ -196,9 +194,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild, paths.hassio_publicPath), publicPath: publicPath(latestBuild, paths.hassio_publicPath),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
defineOverlay: {
__SUPERVISOR__: true,
},
}; };
}, },
@@ -211,9 +206,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild), publicPath: publicPath(latestBuild),
isProdBuild, isProdBuild,
latestBuild, latestBuild,
defineOverlay: {
__DEMO__: true,
},
}; };
}, },
}; };

View File

@@ -5,7 +5,6 @@ const env = require("../env");
require("./clean.js"); require("./clean.js");
require("./translations.js"); require("./translations.js");
require("./locale-data.js");
require("./gen-icons-json.js"); require("./gen-icons-json.js");
require("./gather-static.js"); require("./gather-static.js");
require("./compress.js"); require("./compress.js");
@@ -27,8 +26,7 @@ gulp.task(
"gen-icons-json", "gen-icons-json",
"gen-pages-dev", "gen-pages-dev",
"gen-index-app-dev", "gen-index-app-dev",
"build-translations", "build-translations"
"build-locale-data"
), ),
"copy-static-app", "copy-static-app",
env.useWDS() env.useWDS()
@@ -46,7 +44,7 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean", "clean",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
// Don't compress running tests // Don't compress running tests

View File

@@ -18,7 +18,7 @@ gulp.task(
}, },
"clean-cast", "clean-cast",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast", "copy-static-cast",
"gen-index-cast-dev", "gen-index-cast-dev",
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
@@ -33,7 +33,7 @@ gulp.task(
}, },
"clean-cast", "clean-cast",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-cast", "copy-static-cast",
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
"gen-index-cast-prod" "gen-index-cast-prod"

View File

@@ -20,12 +20,7 @@ gulp.task(
}, },
"clean-demo", "clean-demo",
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel( gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"),
"gen-icons-json",
"gen-index-demo-dev",
"build-translations",
"build-locale-data"
),
"copy-static-demo", "copy-static-demo",
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo" env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
) )
@@ -40,7 +35,7 @@ gulp.task(
"clean-demo", "clean-demo",
// Cast needs to be backwards compatible and older HA has no translations // Cast needs to be backwards compatible and older HA has no translations
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations"),
"copy-static-demo", "copy-static-demo",
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
"gen-index-demo-prod" "gen-index-demo-prod"

View File

@@ -154,15 +154,6 @@ gulp.task("gen-index-cast-dev", (done) => {
contentReceiver contentReceiver
); );
const contentMedia = renderCastTemplate("media", {
latestMediaJS: "/frontend_latest/media.js",
es5MediaJS: "/frontend_es5/media.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", { const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: "/frontend_latest/launcher.js", latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js", es5LauncherJS: "/frontend_es5/launcher.js",
@@ -201,15 +192,6 @@ gulp.task("gen-index-cast-prod", (done) => {
contentReceiver contentReceiver
); );
const contentMedia = renderCastTemplate("media", {
latestMediaJS: latestManifest["media.js"],
es5MediaJS: es5Manifest["media.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", { const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: latestManifest["launcher.js"], latestLauncherJS: latestManifest["launcher.js"],
es5LauncherJS: es5Manifest["launcher.js"], es5LauncherJS: es5Manifest["launcher.js"],

View File

@@ -51,7 +51,6 @@ gulp.task(
gulp.parallel( gulp.parallel(
"gen-icons-json", "gen-icons-json",
"build-translations", "build-translations",
"build-locale-data",
"gather-gallery-demos" "gather-gallery-demos"
), ),
"copy-static-gallery", "copy-static-gallery",
@@ -71,7 +70,6 @@ gulp.task(
gulp.parallel( gulp.parallel(
"gen-icons-json", "gen-icons-json",
"build-translations", "build-translations",
"build-locale-data",
"gather-gallery-demos" "gather-gallery-demos"
), ),
"copy-static-gallery", "copy-static-gallery",

View File

@@ -22,18 +22,11 @@ function copyTranslations(staticDir) {
// Translation output // Translation output
fs.copySync( fs.copySync(
polyPath("build/translations/output"), polyPath("build-translations/output"),
staticPath("translations") staticPath("translations")
); );
} }
function copyLocaleData(staticDir) {
const staticPath = genStaticPath(staticDir);
// Locale data output
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
}
function copyMdiIcons(staticDir) { function copyMdiIcons(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
@@ -91,11 +84,6 @@ function copyMapPanel(staticDir) {
); );
} }
gulp.task("copy-locale-data", async () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
});
gulp.task("copy-translations-app", async () => { gulp.task("copy-translations-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
copyTranslations(staticDir); copyTranslations(staticDir);
@@ -106,11 +94,6 @@ gulp.task("copy-translations-supervisor", async () => {
copyTranslations(staticDir); copyTranslations(staticDir);
}); });
gulp.task("copy-locale-data-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
});
gulp.task("copy-static-app", async () => { gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static; const staticDir = paths.app_output_static;
// Basic static files // Basic static files
@@ -120,7 +103,6 @@ gulp.task("copy-static-app", async () => {
copyPolyfills(staticDir); copyPolyfills(staticDir);
copyFonts(staticDir); copyFonts(staticDir);
copyTranslations(staticDir); copyTranslations(staticDir);
copyLocaleData(staticDir);
copyMdiIcons(staticDir); copyMdiIcons(staticDir);
// Panel assets // Panel assets
@@ -141,7 +123,6 @@ gulp.task("copy-static-demo", async () => {
copyMapPanel(paths.demo_output_static); copyMapPanel(paths.demo_output_static);
copyFonts(paths.demo_output_static); copyFonts(paths.demo_output_static);
copyTranslations(paths.demo_output_static); copyTranslations(paths.demo_output_static);
copyLocaleData(paths.demo_output_static);
copyMdiIcons(paths.demo_output_static); copyMdiIcons(paths.demo_output_static);
}); });
@@ -156,7 +137,6 @@ gulp.task("copy-static-cast", async () => {
copyMapPanel(paths.cast_output_static); copyMapPanel(paths.cast_output_static);
copyFonts(paths.cast_output_static); copyFonts(paths.cast_output_static);
copyTranslations(paths.cast_output_static); copyTranslations(paths.cast_output_static);
copyLocaleData(paths.cast_output_static);
copyMdiIcons(paths.cast_output_static); copyMdiIcons(paths.cast_output_static);
}); });
@@ -172,6 +152,5 @@ gulp.task("copy-static-gallery", async () => {
copyMapPanel(paths.gallery_output_static); copyMapPanel(paths.gallery_output_static);
copyFonts(paths.gallery_output_static); copyFonts(paths.gallery_output_static);
copyTranslations(paths.gallery_output_static); copyTranslations(paths.gallery_output_static);
copyLocaleData(paths.gallery_output_static);
copyMdiIcons(paths.gallery_output_static); copyMdiIcons(paths.gallery_output_static);
}); });

View File

@@ -22,40 +22,17 @@ const getMeta = () => {
const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, { const svg = fs.readFileSync(`${ICON_PATH}/${icon.name}.svg`, {
encoding, encoding,
}); });
return { return { path: svg.match(/ d="([^"]+)"/)[1], name: icon.name };
path: svg.match(/ d="([^"]+)"/)[1],
name: icon.name,
tags: icon.tags,
aliases: icon.aliases,
};
}); });
}; };
const addRemovedMeta = (meta) => { const addRemovedMeta = (meta) => {
const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding }); const file = fs.readFileSync(REMOVED_ICONS_PATH, { encoding });
const removed = JSON.parse(file); const removed = JSON.parse(file);
const removedMeta = removed.map((removeIcon) => ({ const combinedMeta = [...meta, ...removed];
path: removeIcon.path,
name: removeIcon.name,
tags: [],
aliases: [],
}));
const combinedMeta = [...meta, ...removedMeta];
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name)); return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
}; };
const homeAutomationTag = "Home Automation";
const orderMeta = (meta) => {
const homeAutomationMeta = meta.filter((icon) =>
icon.tags.includes(homeAutomationTag)
);
const otherMeta = meta.filter(
(icon) => !icon.tags.includes(homeAutomationTag)
);
return [...homeAutomationMeta, ...otherMeta];
};
const splitBySize = (meta) => { const splitBySize = (meta) => {
const chunks = []; const chunks = [];
const CHUNK_SIZE = 50000; const CHUNK_SIZE = 50000;
@@ -100,10 +77,8 @@ const findDifferentiator = (curString, prevString) => {
}; };
gulp.task("gen-icons-json", (done) => { gulp.task("gen-icons-json", (done) => {
const meta = getMeta(); const meta = addRemovedMeta(getMeta());
const split = splitBySize(meta);
const metaAndRemoved = addRemovedMeta(meta);
const split = splitBySize(metaAndRemoved);
if (!fs.existsSync(OUTPUT_DIR)) { if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.mkdirSync(OUTPUT_DIR, { recursive: true });
@@ -141,18 +116,5 @@ gulp.task("gen-icons-json", (done) => {
JSON.stringify({ version: package.version, parts }) JSON.stringify({ version: package.version, parts })
); );
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconList.json"),
JSON.stringify(
orderMeta(meta).map((icon) => ({
name: icon.name,
keywords: [
...icon.tags.map((t) => t.toLowerCase().replace(/\s\/\s/g, " ")),
...icon.aliases,
],
}))
)
);
done(); done();
}); });

View File

@@ -1,6 +1,9 @@
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs");
const path = require("path");
const env = require("../env"); const env = require("../env");
const paths = require("../paths");
require("./clean.js"); require("./clean.js");
require("./gen-icons-json.js"); require("./gen-icons-json.js");
@@ -17,11 +20,10 @@ gulp.task(
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
}, },
"clean-hassio", "clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev", "gen-index-hassio-dev",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio" env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
) )
); );
@@ -33,10 +35,9 @@ gulp.task(
process.env.NODE_ENV = "production"; process.env.NODE_ENV = "production";
}, },
"clean-hassio", "clean-hassio",
"gen-icons-json",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data",
"copy-locale-data-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod", "gen-index-hassio-prod",
...// Don't compress running tests ...// Don't compress running tests

View File

@@ -1,74 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const del = require("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const paths = require("../paths");
const outDir = "build/locale-data";
gulp.task("clean-locale-data", () => del([outDir]));
gulp.task("ensure-locale-data-build-dir", (done) => {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
done();
});
const modules = {
"intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat",
};
gulp.task("create-locale-data", (done) => {
const translationMeta = JSON.parse(
fs.readFileSync(
path.join(paths.translations_src, "translationMetadata.json")
)
);
Object.entries(modules).forEach(([module, className]) => {
Object.keys(translationMeta).forEach((lang) => {
try {
const localeData = String(
fs.readFileSync(
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
)
)
.replace(
new RegExp(
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
"im"
),
""
)
.replace(/\)\s*}/im, "");
// make sure we have valid JSON
JSON.parse(localeData);
if (!fs.existsSync(path.join(outDir, module))) {
fs.mkdirSync(path.join(outDir, module), { recursive: true });
}
fs.writeFileSync(
path.join(outDir, `${module}/${lang}.json`),
localeData
);
} catch (e) {
if (e.code !== "MODULE_NOT_FOUND") {
throw e;
}
}
});
done();
});
});
gulp.task(
"build-locale-data",
gulp.series(
"clean-locale-data",
"ensure-locale-data-build-dir",
"create-locale-data"
)
);

View File

@@ -17,7 +17,7 @@ const paths = require("../paths");
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
const workDir = "build/translations"; const workDir = "build-translations";
const fullDir = workDir + "/full"; const fullDir = workDir + "/full";
const coreDir = workDir + "/core"; const coreDir = workDir + "/core";
const outDir = workDir + "/output"; const outDir = workDir + "/output";
@@ -121,7 +121,7 @@ gulp.task("clean-translations", () => del([workDir]));
gulp.task("ensure-translations-build-dir", (done) => { gulp.task("ensure-translations-build-dir", (done) => {
if (!fs.existsSync(workDir)) { if (!fs.existsSync(workDir)) {
fs.mkdirSync(workDir, { recursive: true }); fs.mkdirSync(workDir);
} }
done(); done();
}); });

View File

@@ -35,29 +35,26 @@ const isWsl =
* listenHost?: string * listenHost?: string
* }} * }}
*/ */
const runDevServer = async ({ const runDevServer = ({
compiler, compiler,
contentBase, contentBase,
port, port,
listenHost = "localhost", listenHost = "localhost",
}) => { }) =>
const server = new WebpackDevServer( new WebpackDevServer(compiler, {
{ open: true,
open: true, watchContentBase: true,
host: listenHost, contentBase,
port, }).listen(port, listenHost, (err) => {
static: { if (err) {
directory: contentBase, throw err;
watch: true, }
}, // Server listening
}, log(
compiler "[webpack-dev-server]",
); `Project is running at http://localhost:${port}`
);
await server.start(); });
// Server listening
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
};
const doneHandler = (done) => (err, stats) => { const doneHandler = (done) => (err, stats) => {
if (err) { if (err) {
@@ -110,13 +107,13 @@ gulp.task("webpack-prod-app", () =>
) )
); );
gulp.task("webpack-dev-server-demo", () => gulp.task("webpack-dev-server-demo", () => {
runDevServer({ runDevServer({
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
contentBase: paths.demo_output_root, contentBase: paths.demo_output_root,
port: 8090, port: 8090,
}) });
); });
gulp.task("webpack-prod-demo", () => gulp.task("webpack-prod-demo", () =>
prodBuild( prodBuild(
@@ -126,15 +123,15 @@ gulp.task("webpack-prod-demo", () =>
) )
); );
gulp.task("webpack-dev-server-cast", () => gulp.task("webpack-dev-server-cast", () => {
runDevServer({ runDevServer({
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
contentBase: paths.cast_output_root, contentBase: paths.cast_output_root,
port: 8080, port: 8080,
// Accessible from the network, because that's how Cast hits it. // Accessible from the network, because that's how Cast hits it.
listenHost: "0.0.0.0", listenHost: "0.0.0.0",
}) });
); });
gulp.task("webpack-prod-cast", () => gulp.task("webpack-prod-cast", () =>
prodBuild( prodBuild(
@@ -151,7 +148,7 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({ ignored: /build/, poll: isWsl }, doneHandler()); ).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
@@ -167,15 +164,14 @@ gulp.task("webpack-prod-hassio", () =>
) )
); );
gulp.task("webpack-dev-server-gallery", () => gulp.task("webpack-dev-server-gallery", () => {
runDevServer({ runDevServer({
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't // We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })), compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
contentBase: paths.gallery_output_root, contentBase: paths.gallery_output_root,
port: 8100, port: 8100,
listenHost: "0.0.0.0", });
}) });
);
gulp.task("webpack-prod-gallery", () => gulp.task("webpack-prod-gallery", () =>
prodBuild( prodBuild(

File diff suppressed because one or more lines are too long

View File

@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<style>
body {
--logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--logo-repeat: no-repeat;
--playback-logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--theme-hue: 200;
--progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover;
--background-color: #41bdf5;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head>
<body>
<%= renderTemplate('_js_base') %>
<cast-media-player></cast-media-player>
<script>
import("<%= latestMediaJS %>");
window.latestJS = true;
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5MediaJS %>");
};
<% } else { %>
_ls("<%= es5MediaJS %>");
<% } %>
}
</script>
</body>
</html>

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiCast, mdiCastConnected } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket"; import { Auth, Connection } from "home-assistant-js-websocket";
@@ -18,7 +17,6 @@ import {
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
import "../../../../src/components/ha-icon"; import "../../../../src/components/ha-icon";
import "../../../../src/components/ha-svg-icon";
import { import {
getLegacyLovelaceCollection, getLegacyLovelaceCollection,
getLovelaceCollection, getLovelaceCollection,
@@ -75,7 +73,7 @@ class HcCast extends LitElement {
? html` ? html`
<p class="center-item"> <p class="center-item">
<mwc-button raised @click=${this._handleLaunch}> <mwc-button raised @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCast}></ha-svg-icon> <ha-icon icon="hass:cast"></ha-icon>
Start Casting Start Casting
</mwc-button> </mwc-button>
</p> </p>
@@ -113,7 +111,7 @@ class HcCast extends LitElement {
${this.castManager.status ${this.castManager.status
? html` ? html`
<mwc-button @click=${this._handleLaunch}> <mwc-button @click=${this._handleLaunch}>
<ha-svg-icon .path=${mdiCastConnected}></ha-svg-icon> <ha-icon icon="hass:cast-connected"></ha-icon>
Manage Manage
</mwc-button> </mwc-button>
` `
@@ -235,7 +233,7 @@ class HcCast extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
mwc-button ha-svg-icon { mwc-button ha-icon {
margin-right: 8px; margin-right: 8px;
height: 18px; height: 18px;
} }

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCastConnected, mdiCast } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import { import {
Auth, Auth,
@@ -20,7 +19,7 @@ import {
loadTokens, loadTokens,
saveTokens, saveTokens,
} from "../../../../src/common/auth/token_storage"; } from "../../../../src/common/auth/token_storage";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-icon";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import { registerServiceWorker } from "../../../../src/util/register-service-worker"; import { registerServiceWorker } from "../../../../src/util/register-service-worker";
import "./hc-layout"; import "./hc-layout";
@@ -128,11 +127,11 @@ export class HcConnect extends LitElement {
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._handleDemo}> <mwc-button @click=${this._handleDemo}>
Show Demo Show Demo
<ha-svg-icon <ha-icon
.path=${this.castManager.castState === "CONNECTED" .icon=${this.castManager.castState === "CONNECTED"
? mdiCastConnected ? "hass:cast-connected"
: mdiCast} : "hass:cast"}
></ha-svg-icon> ></ha-icon>
</mwc-button> </mwc-button>
<div class="spacer"></div> <div class="spacer"></div>
<mwc-button @click=${this._handleConnect}>Authorize</mwc-button> <mwc-button @click=${this._handleConnect}>Authorize</mwc-button>
@@ -308,7 +307,7 @@ export class HcConnect extends LitElement {
color: darkred; color: darkred;
} }
mwc-button ha-svg-icon { mwc-button ha-icon {
margin-left: 8px; margin-left: 8px;
} }

View File

@@ -1,22 +0,0 @@
const castContext = cast.framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
castContext.start();

View File

@@ -8,9 +8,6 @@ import { ReceivedMessage } from "./types";
const lovelaceController = new HcMain(); const lovelaceController = new HcMain();
document.body.append(lovelaceController); document.body.append(lovelaceController);
lovelaceController.addEventListener("cast-view-changed", (ev) => {
playDummyMedia(ev.detail.title);
});
const mediaPlayer = document.createElement("cast-media-player"); const mediaPlayer = document.createElement("cast-media-player");
mediaPlayer.style.display = "none"; mediaPlayer.style.display = "none";
@@ -31,31 +28,6 @@ const setTouchControlsVisibility = (visible: boolean) => {
} }
}; };
let timeOut: number | undefined;
const playDummyMedia = (viewTitle?: string) => {
const loadRequestData = new cast.framework.messages.LoadRequestData();
loadRequestData.autoplay = true;
loadRequestData.media = new cast.framework.messages.MediaInformation();
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
const metadata = new cast.framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
loadRequestData.requestId = 0;
playerManager.load(loadRequestData);
if (timeOut) {
clearTimeout(timeOut);
timeOut = undefined;
}
if (castContext.getDeviceCapabilities().touch_input_supported) {
timeOut = window.setTimeout(() => playDummyMedia(viewTitle), 540000); // repeat every 9 minutes to keep it active (gets deactivated after 10 minutes)
}
};
const showLovelaceController = () => { const showLovelaceController = () => {
mediaPlayer.style.display = "none"; mediaPlayer.style.display = "none";
lovelaceController.style.display = "initial"; lovelaceController.style.display = "initial";
@@ -79,7 +51,6 @@ const showMediaPlayer = () => {
--progress-color: #03a9f4; --progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png'); --splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover; --splash-size: cover;
--background-color: #41bdf5;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -92,6 +63,22 @@ options.customNamespaces = {
[CAST_NS]: cast.framework.system.MessageType.JSON, [CAST_NS]: cast.framework.system.MessageType.JSON,
}; };
// The docs say we need to set options.touchScreenOptimizeApp = true
// https://developers.google.com/cast/docs/caf_receiver/customize_ui#accessing_ui_controls
// This doesn't work.
// @ts-ignore
options.touchScreenOptimizedApp = true;
// The class reference say we can set a uiConfig in options to set it
// https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.CastReceiverOptions#uiConfig
// This doesn't work either.
// @ts-ignore
options.uiConfig = new cast.framework.ui.UiConfig();
// @ts-ignore
options.uiConfig.touchScreenOptimizedApp = true;
castContext.setInactivityTimeout(86400); // 1 day
castContext.addCustomMessageListener( castContext.addCustomMessageListener(
CAST_NS, CAST_NS,
// @ts-ignore // @ts-ignore
@@ -116,12 +103,6 @@ const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor( playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD, cast.framework.messages.MessageType.LOAD,
(loadRequestData) => { (loadRequestData) => {
if (
loadRequestData.media.contentId ===
"https://cast.home-assistant.io/images/google-nest-hub.png"
) {
return loadRequestData;
}
// We received a play media command, hide Lovelace and show media player // We received a play media command, hide Lovelace and show media player
showMediaPlayer(); showMediaPlayer();
const media = loadRequestData.media; const media = loadRequestData.media;

View File

@@ -1,6 +1,5 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace"; import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types"; import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view"; import "../../../../src/panels/lovelace/views/hui-view";
@@ -15,7 +14,7 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number; @property() public viewPath?: string | number;
@property() public urlPath: string | null = null; public urlPath?: string | null;
protected render(): TemplateResult { protected render(): TemplateResult {
const index = this._viewIndex; const index = this._viewIndex;
@@ -31,7 +30,7 @@ class HcLovelace extends LitElement {
config: this.lovelaceConfig, config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig, rawConfig: this.lovelaceConfig,
editMode: false, editMode: false,
urlPath: this.urlPath, urlPath: this.urlPath!,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
locale: this.hass.locale, locale: this.hass.locale,
@@ -55,21 +54,6 @@ class HcLovelace extends LitElement {
const index = this._viewIndex; const index = this._viewIndex;
if (index !== undefined) { if (index !== undefined) {
const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
const viewTitle =
this.lovelaceConfig.views[index].title ||
this.lovelaceConfig.views[index].path;
fireEvent(this, "cast-view-changed", {
title:
dashboardTitle || viewTitle
? `${dashboardTitle || ""}${
dashboardTitle && viewTitle ? ": " : ""
}${viewTitle || ""}`
: undefined,
});
const configBackground = const configBackground =
this.lovelaceConfig.views[index].background || this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background; this.lovelaceConfig.background;
@@ -117,15 +101,8 @@ class HcLovelace extends LitElement {
} }
} }
export interface CastViewChanged {
title: string | undefined;
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hc-lovelace": HcLovelace; "hc-lovelace": HcLovelace;
} }
interface HASSDomEvents {
"cast-view-changed": CastViewChanged;
}
} }

View File

@@ -13,11 +13,7 @@ import {
ShowDemoMessage, ShowDemoMessage,
ShowLovelaceViewMessage, ShowLovelaceViewMessage,
} from "../../../../src/cast/receiver_messages"; } from "../../../../src/cast/receiver_messages";
import { import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages";
ReceiverErrorCode,
ReceiverErrorMessage,
ReceiverStatusMessage,
} from "../../../../src/cast/sender_messages";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import { import {
@@ -44,10 +40,10 @@ export class HcMain extends HassElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _urlPath?: string | null;
private _unsubLovelace?: UnsubscribeFunc; private _unsubLovelace?: UnsubscribeFunc;
private _urlPath?: string | null;
public processIncomingMessage(msg: HassMessage) { public processIncomingMessage(msg: HassMessage) {
if (msg.type === "connect") { if (msg.type === "connect") {
this._handleConnectMessage(msg); this._handleConnectMessage(msg);
@@ -72,10 +68,8 @@ export class HcMain extends HassElement {
!this._lovelaceConfig || !this._lovelaceConfig ||
this._lovelacePath === null || this._lovelacePath === null ||
// Guard against part of HA not being loaded yet. // Guard against part of HA not being loaded yet.
!this.hass || (this.hass &&
!this.hass.states || (!this.hass.states || !this.hass.config || !this.hass.services))
!this.hass.config ||
!this.hass.services
) { ) {
return html` return html`
<hc-launch-screen <hc-launch-screen
@@ -113,7 +107,6 @@ export class HcMain extends HassElement {
this._sendStatus(); this._sendStatus();
} }
}); });
this.addEventListener("dialog-closed", this._dialogClosed);
} }
private _sendStatus(senderId?: string) { private _sendStatus(senderId?: string) {
@@ -125,7 +118,7 @@ export class HcMain extends HassElement {
if (this.hass) { if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl; status.hassUrl = this.hass.auth.data.hassUrl;
status.lovelacePath = this._lovelacePath; status.lovelacePath = this._lovelacePath!;
status.urlPath = this._urlPath; status.urlPath = this._urlPath;
} }
@@ -138,30 +131,6 @@ export class HcMain extends HassElement {
} }
} }
private _sendError(
error_code: number,
error_message: string,
senderId?: string
) {
const error: ReceiverErrorMessage = {
type: "receiver_error",
error_code,
error_message,
};
if (senderId) {
this.sendMessage(senderId, error);
} else {
for (const sender of castContext.getSenders()) {
this.sendMessage(sender.id, error);
}
}
}
private _dialogClosed = () => {
document.body.setAttribute("style", "overflow-y: auto !important");
};
private async _handleGetStatusMessage(msg: GetStatusMessage) { private async _handleGetStatusMessage(msg: GetStatusMessage) {
this._sendStatus(msg.senderId!); this._sendStatus(msg.senderId!);
} }
@@ -180,18 +149,14 @@ export class HcMain extends HassElement {
}), }),
}); });
} catch (err: any) { } catch (err: any) {
const errorMessage = this._getErrorMessage(err); this._error = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return; return;
} }
let connection; let connection;
try { try {
connection = await createConnection({ auth }); connection = await createConnection({ auth });
} catch (err: any) { } catch (err: any) {
const errorMessage = this._getErrorMessage(err); this._error = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return; return;
} }
if (this.hass) { if (this.hass) {
@@ -203,29 +168,24 @@ export class HcMain extends HassElement {
} }
private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) { private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) {
this._showDemo = false;
// We should not get this command before we are connected. // We should not get this command before we are connected.
// Means a client got out of sync. Let's send status to them. // Means a client got out of sync. Let's send status to them.
if (!this.hass) { if (!this.hass) {
this._sendStatus(msg.senderId!); this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected."; this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
return; return;
} }
this._error = undefined;
if (msg.urlPath === "lovelace") { if (msg.urlPath === "lovelace") {
msg.urlPath = null; msg.urlPath = null;
} }
this._lovelacePath = msg.viewPath;
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) { if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath; this._urlPath = msg.urlPath;
this._lovelaceConfig = undefined;
if (this._unsubLovelace) { if (this._unsubLovelace) {
this._unsubLovelace(); this._unsubLovelace();
} }
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107) const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? getLovelaceCollection(this.hass.connection, msg.urlPath) ? getLovelaceCollection(this.hass!.connection, msg.urlPath)
: getLegacyLovelaceCollection(this.hass.connection); : getLegacyLovelaceCollection(this.hass!.connection);
// We first do a single refresh because we need to check if there is LL // We first do a single refresh because we need to check if there is LL
// configuration. // configuration.
try { try {
@@ -234,16 +194,8 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig) this._handleNewLovelaceConfig(lovelaceConfig)
); );
} catch (err: any) { } catch (err: any) {
if ( // eslint-disable-next-line
atLeastVersion(this.hass.connection.haVersion, 0, 107) && console.log("Error fetching Lovelace configuration", err, msg);
err.code !== "config_not_found"
) {
// eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg);
this._error = `Error fetching Lovelace configuration: ${err.message}`;
this._sendError(ReceiverErrorCode.FETCH_CONFIG_FAILED, this._error);
return;
}
// Generate a Lovelace config. // Generate a Lovelace config.
this._unsubLovelace = () => undefined; this._unsubLovelace = () => undefined;
await this._generateLovelaceConfig(); await this._generateLovelaceConfig();
@@ -258,6 +210,8 @@ export class HcMain extends HassElement {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl); loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
} }
} }
this._showDemo = false;
this._lovelacePath = msg.viewPath;
this._sendStatus(); this._sendStatus();
} }
@@ -278,7 +232,7 @@ export class HcMain extends HassElement {
} }
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
castContext.setApplicationState(lovelaceConfig.title || ""); castContext.setApplicationState(lovelaceConfig.title!);
this._lovelaceConfig = lovelaceConfig; this._lovelaceConfig = lovelaceConfig;
} }

View File

@@ -1,4 +1,3 @@
import { mdiTelevision } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { CastManager } from "../../../src/cast/cast_manager"; import { CastManager } from "../../../src/cast/cast_manager";
@@ -28,7 +27,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
return html``; return html``;
} }
return html` return html`
<ha-svg-icon .path=${mdiTelevision}></ha-svg-icon> <ha-icon icon="hademo:television"></ha-icon>
<div class="flex"> <div class="flex">
<div class="name">Show Chromecast interface</div> <div class="name">Show Chromecast interface</div>
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
@@ -73,7 +72,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
display: flex; display: flex;
align-items: center; align-items: center;
} }
ha-svg-icon { ha-icon {
padding: 8px; padding: 8px;
color: var(--paper-item-icon-color); color: var(--paper-item-icon-color);
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
import { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (
hass: MockHomeAssistant,
data: AreaRegistryEntry[] = []
) => hass.mockWS("config/area_registry/list", () => data);

View File

@@ -1,7 +0,0 @@
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockDeviceRegistry = (
hass: MockHomeAssistant,
data: DeviceRegistryEntry[] = []
) => hass.mockWS("config/device_registry/list", () => data);

View File

@@ -1,7 +0,0 @@
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockEntityRegistry = (
hass: MockHomeAssistant,
data: EntityRegistryEntry[] = []
) => hass.mockWS("config/entity_registry/list", () => data);

View File

@@ -1,59 +0,0 @@
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
hass.config.components.push("hassio");
hass.mockWS("supervisor/api", (msg) => {
if (msg.endpoint === "/supervisor/info") {
const data: HassioSupervisorInfo = {
version: "2021.10.dev0805",
version_latest: "2021.10.dev0806",
update_available: true,
channel: "dev",
arch: "aarch64",
supported: true,
healthy: true,
ip_address: "172.30.32.2",
wait_boot: 5,
timezone: "America/Los_Angeles",
logging: "info",
debug: false,
debug_block: false,
diagnostics: true,
addons: [
{
name: "Visual Studio Code",
slug: "a0d7b954_vscode",
description:
"Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!",
state: "started",
version: "3.6.2",
version_latest: "3.6.2",
update_available: false,
repository: "a0d7b954",
icon: true,
logo: true,
},
{
name: "Z-Wave JS",
slug: "core_zwave_js",
description:
"Control a ZWave network with Home Assistant Z-Wave JS",
state: "started",
version: "0.1.45",
version_latest: "0.1.45",
update_available: false,
repository: "core",
icon: true,
logo: true,
},
] as any,
addons_repositories: [
"https://github.com/hassio-addons/repository",
] as any,
};
return data;
}
return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`);
});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -1,35 +0,0 @@
#!/bin/bash
TARGET_LABEL="Needs gallery preview"
if [[ "$NETLIFY" != "true" ]]; then
echo "This script can only be run on Netlify"
exit 1
fi
function createStatus() {
state="$1"
description="$2"
target_url="$3"
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \
-d '{"state": "'"${state}"'", "context": "Netlify/Gallery Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}'
}
if [[ "${PULL_REQUEST}" == "false" ]]; then
gulp build-gallery
else
if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then
createStatus "pending" "Building gallery preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
gulp build-gallery
if [ $? -eq 0 ]; then
createStatus "success" "Build complete" "$DEPLOY_URL"
else
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
fi
else
createStatus "success" "Build was not requested by PR label"
fi
fi

View File

@@ -1,143 +0,0 @@
import { Button } from "@material/mwc-button";
import { html, LitElement, css, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
@property() title!: string;
@property() value!: any;
@property() disabled = false;
protected render(): TemplateResult {
return html`
<div class="row">
<div class="content light">
<ha-card .header=${this.title}>
<div class="card-content">
<slot name="light"></slot>
</div>
<div class="card-actions">
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</mwc-button>
</div>
</ha-card>
</div>
<div class="content dark">
<ha-card .header=${this.title}>
<div class="card-content">
<slot name="dark"></slot>
</div>
<div class="card-actions">
<mwc-button
.disabled=${this.disabled}
@click=${this.handleSubmit}
>
Submit
</mwc-button>
</div>
</ha-card>
<pre>${JSON.stringify(this.value, undefined, 2)}</pre>
</div>
</div>
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
handleSubmit(ev) {
const content = (ev.target as Button).closest(".content")!;
fireEvent(this, "submitted" as any, {
slot: content.classList.contains("light") ? "light" : "dark",
});
}
static styles = css`
.row {
display: flex;
}
.content {
padding: 50px 0;
background-color: var(--primary-background-color);
}
.light {
flex: 1;
padding-left: 50px;
padding-right: 50px;
box-sizing: border-box;
}
.light ha-card {
margin-left: auto;
}
.dark {
display: flex;
flex: 1;
padding-left: 50px;
box-sizing: border-box;
flex-wrap: wrap;
}
ha-card {
width: 400px;
}
pre {
width: 300px;
margin: 0 16px 0;
overflow: auto;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
flex-direction: row-reverse;
border-top: none;
}
@media only screen and (max-width: 1500px) {
.light {
flex: initial;
}
}
@media only screen and (max-width: 1000px) {
.light,
.dark {
padding: 16px;
}
.row,
.dark {
flex-direction: column;
}
ha-card {
margin: 0 auto;
width: 100%;
max-width: 400px;
}
pre {
margin: 16px auto;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-black-white-row": DemoBlackWhiteRow;
}
}

View File

@@ -1,91 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import "../../../src/panels/config/automation/action/ha-automation-action";
import { HaChooseAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-choose";
import { HaDelayAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-delay";
import { HaDeviceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
import { HaEventAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-event";
import { HaRepeatAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
import { HaSceneAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-scene";
import { HaServiceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-service";
import { HaWaitForTriggerAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
import { HaWaitAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../src/data/script";
import { HaConditionAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-condition";
const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Event", actions: [HaEventAction.defaultConfig] },
{ name: "Device", actions: [HaDeviceAction.defaultConfig] },
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
];
@customElement("demo-automation-editor-action")
class DemoHaAutomationEditorAction extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.actions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-action
slot=${slot}
.hass=${this.hass}
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-action>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
}
}

View File

@@ -1,127 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Condition } from "../../../src/data/automation";
import "../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
import { HaTemplateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-template";
import { HaTimeCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
},
{
name: "Numeric State",
conditions: [
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
},
{
name: "Sun",
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
},
{
name: "Zone",
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
},
{
name: "Time",
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
},
{
name: "Template",
conditions: [
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
},
{
name: "Device",
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
},
{
name: "And",
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Or",
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Not",
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
];
@customElement("demo-automation-editor-condition")
class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.conditions);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-condition
slot=${slot}
.hass=${this.hass}
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-condition>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
}
}

View File

@@ -1,159 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import { LitElement, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
import type { Trigger } from "../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
import { HaHassTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaNumericStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaSunTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-sun";
import { HaTagTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-tag";
import { HaTemplateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-template";
import { HaTimeTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
import { HaTimePatternTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
import { HaWebhookTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
import { HaZoneTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
import { HaDeviceTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
import { HaStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
import { HaMQTTTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../src/panels/config/automation/trigger/ha-automation-trigger";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "State",
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
},
{
name: "Home Assistant",
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
},
{
name: "Numeric State",
triggers: [
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
},
{
name: "Sun",
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
},
{
name: "Time Pattern",
triggers: [
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
},
{
name: "Webhook",
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Zone",
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
},
{
name: "Tag",
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
},
{
name: "Time",
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
},
{
name: "Template",
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
},
{
name: "Event",
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
},
{
name: "Device Trigger",
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant;
private data: any = SCHEMAS.map((info) => info.triggers);
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
${SCHEMAS.map(
(info, sampleIdx) => html`
<demo-black-white-row
.title=${info.name}
.value=${this.data[sampleIdx]}
>
${["light", "dark"].map(
(slot) =>
html`
<ha-automation-trigger
slot=${slot}
.hass=${this.hass}
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
@value-changed=${valueChanged}
></ha-automation-trigger>
`
)}
</demo-black-white-row>
`
)}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
}
}

View File

@@ -1,19 +1,15 @@
import "@material/mwc-button/mwc-button"; import { html, css, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-logo-svg";
const alerts: { const alerts: {
title?: string; title?: string;
description: string | TemplateResult; description: string | TemplateResult;
type: "info" | "warning" | "error" | "success"; type: "info" | "warning" | "error" | "success";
dismissable?: boolean; dismissable?: boolean;
action?: string;
rtl?: boolean; rtl?: boolean;
iconSlot?: TemplateResult;
actionSlot?: TemplateResult;
}[] = [ }[] = [
{ {
title: "Test info alert", title: "Test info alert",
@@ -77,35 +73,13 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action", description: "This is a test error alert with action",
type: "error", type: "error",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`, action: "restart",
}, },
{ {
title: "Unsaved data", title: "Unsaved data",
description: "You have unsaved data", description: "You have unsaved data",
type: "warning", type: "warning",
actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`, action: "save",
},
{
title: "Slotted icon",
description: "Alert with slotted icon",
type: "warning",
iconSlot: html`<span slot="icon" class="image">
<ha-logo-svg></ha-logo-svg>
</span>`,
},
{
title: "Slotted image",
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{
title: "Slotted action",
description: "Alert with slotted action",
type: "info",
actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
}, },
{ {
description: "Dismissable information (RTL)", description: "Dismissable information (RTL)",
@@ -117,7 +91,7 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action (RTL)", description: "This is a test error alert with action (RTL)",
type: "error", type: "error",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`, action: "restart",
rtl: true, rtl: true,
}, },
{ {
@@ -132,83 +106,38 @@ const alerts: {
export class DemoHaAlert extends LitElement { export class DemoHaAlert extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${["light", "dark"].map( <ha-card header="ha-alert demo">
(mode) => html` ${alerts.map(
<div class=${mode}> (alert) => html`
<ha-card header="ha-alert ${mode} demo"> <ha-alert
<div class="card-content"> .title=${alert.title || ""}
${alerts.map( .alertType=${alert.type}
(alert) => html` .dismissable=${alert.dismissable || false}
<ha-alert .actionText=${alert.action || ""}
.title=${alert.title || ""} .rtl=${alert.rtl || false}
.alertType=${alert.type} >
.dismissable=${alert.dismissable || false} ${alert.description}
.rtl=${alert.rtl || false} </ha-alert>
> `
${alert.iconSlot} ${alert.description} ${alert.actionSlot} )}
</ha-alert> </ha-card>
`
)}
</div>
</ha-card>
</div>
`
)}
`; `;
} }
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
static get styles() { static get styles() {
return css` return css`
:host {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-card { ha-card {
max-width: 600px;
margin: 24px auto; margin: 24px auto;
} }
ha-alert {
display: block;
margin: 24px 0;
}
.condition { .condition {
padding: 16px; padding: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.image { span {
display: inline-flex; margin-right: 16px;
height: 100%;
align-items: center;
}
img {
max-height: 24px;
width: 24px;
}
mwc-button {
--mdc-theme-primary: var(--primary-text-color);
} }
`; `;
} }

View File

@@ -1,85 +0,0 @@
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../src/components/ha-bar";
import "../../../src/components/ha-card";
const bars: {
min?: number;
max?: number;
value: number;
warning?: number;
error?: number;
}[] = [
{
value: 33,
},
{
value: 150,
},
{
min: -10,
value: 0,
},
{
value: 80,
},
{
value: 200,
max: 13,
},
{
value: 4,
min: 13,
},
];
@customElement("demo-ha-bar")
export class DemoHaBar extends LitElement {
protected render(): TemplateResult {
return html`
${bars
.map((bar) => ({ min: 0, max: 100, warning: 70, error: 90, ...bar }))
.map(
(bar) => html`
<ha-card>
<div class="card-content">
<pre>Config: ${JSON.stringify(bar)}</pre>
<ha-bar
class=${classMap({
warning: bar.value > bar.warning,
error: bar.value > bar.error,
})}
.min=${bar.min}
.max=${bar.max}
.value=${bar.value}
>
</ha-bar>
</div>
</ha-card>
`
)}
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.warning {
--ha-bar-primary-color: var(--warning-color);
}
.error {
--ha-bar-primary-color: var(--error-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-bar": DemoHaBar;
}
}

View File

@@ -1,86 +0,0 @@
import { mdiHomeAssistant } from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/components/ha-chip";
import "../../../src/components/ha-chip-set";
import "../../../src/components/ha-svg-icon";
const chips: {
icon?: string;
content?: string;
}[] = [
{},
{
icon: mdiHomeAssistant,
},
{
content: "Content",
},
{
icon: mdiHomeAssistant,
content: "Content",
},
];
@customElement("demo-ha-chips")
export class DemoHaChips extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-chip demo">
<div class="card-content">
${chips.map(
(chip) => html`
<ha-chip .hasIcon=${chip.icon !== undefined}>
${chip.icon
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
</ha-svg-icon>`
: ""}
${chip.content}
</ha-chip>
`
)}
</div>
</ha-card>
<ha-card header="ha-chip-set demo">
<div class="card-content">
<ha-chip-set>
${chips.map(
(chip) => html`
<ha-chip .hasIcon=${chip.icon !== undefined}>
${chip.icon
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
</ha-svg-icon>`
: ""}
${chip.content}
</ha-chip>
`
)}
</ha-chip-set>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
ha-chip {
margin-bottom: 4px;
}
.card-content {
display: flex;
flex-direction: column;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-chips": DemoHaChips;
}
}

View File

@@ -1,306 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../../../src/components/ha-form/types";
import "../../../src/components/ha-form/ha-form";
import "../components/demo-black-white-row";
const SCHEMAS: {
title: string;
translations?: Record<string, string>;
error?: Record<string, string>;
schema: HaFormSchema[];
data?: Record<string, any>;
}[] = [
{
title: "Authentication",
translations: {
username: "Username",
password: "Password",
invalid_login: "Invalid username or password",
},
error: {
base: "invalid_login",
},
schema: [
{
type: "string",
name: "username",
required: true,
},
{
type: "string",
name: "password",
required: true,
},
],
},
{
title: "One of each",
schema: [
{
type: "constant",
value: "Constant Value",
name: "constant",
required: true,
},
{
type: "boolean",
name: "bool",
optional: true,
default: false,
},
{
type: "integer",
name: "int",
optional: true,
default: 10,
},
{
type: "float",
name: "float",
required: true,
},
{
type: "string",
name: "string",
optional: true,
default: "Default",
},
{
type: "select",
options: [
["default", "default"],
["other", "other"],
],
name: "select",
optional: true,
default: "default",
},
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
},
name: "multi",
optional: true,
default: ["default"],
},
{
type: "positive_time_period_dict",
name: "time",
required: true,
},
],
},
{
title: "Numbers",
schema: [
{
type: "integer",
name: "int",
required: true,
},
{
type: "integer",
name: "int with default",
optional: true,
default: 10,
},
{
type: "integer",
name: "int range required",
required: true,
default: 5,
valueMin: 0,
valueMax: 10,
},
{
type: "integer",
name: "int range optional",
optional: true,
valueMin: 0,
valueMax: 10,
},
],
},
{
title: "select",
schema: [
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
],
name: "select",
required: true,
default: "default",
},
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
],
name: "select optional",
optional: true,
},
{
type: "select",
options: [
["default", "Default"],
["other", "Other"],
["uno", "mas"],
["one", "more"],
["and", "another_one"],
["option", "1000"],
],
name: "select many otions",
optional: true,
default: "default",
},
],
},
{
title: "Multi select",
schema: [
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
},
name: "multi",
required: true,
default: ["default"],
},
{
type: "multi_select",
options: {
default: "Default",
other: "Other",
uno: "mas",
one: "more",
and: "another_one",
option: "1000",
},
name: "multi many otions",
optional: true,
default: ["default"],
},
],
},
{
title: "Field specific error",
data: {
new_password: "hello",
new_password_2: "bye",
},
translations: {
new_password: "New Password",
new_password_2: "Re-type Password",
not_match: "The passwords do not match",
},
error: {
new_password_2: "not_match",
},
schema: [
{
type: "string",
name: "new_password",
required: true,
},
{
type: "string",
name: "new_password_2",
required: true,
},
],
},
{
title: "OctoPrint",
translations: {
username: "Username",
host: "Host",
port: "Port Number",
path: "Application Path",
ssl: "Use SSL",
},
schema: [
{ type: "string", name: "username", required: true, default: "" },
{ type: "string", name: "host", required: true, default: "" },
{
type: "integer",
valueMin: 1,
valueMax: 65535,
name: "port",
optional: true,
default: 80,
},
{ type: "string", name: "path", optional: true, default: "/" },
{ type: "boolean", name: "ssl", optional: true, default: false },
],
},
];
@customElement("demo-ha-form")
class DemoHaForm extends LitElement {
private data = SCHEMAS.map(
({ schema, data }) => data || computeInitialHaFormData(schema)
);
private disabled = SCHEMAS.map(() => false);
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const translations = info.translations || {};
return html`
<demo-black-white-row
.title=${info.title}
.value=${this.data[idx]}
.disabled=${this.disabled[idx]}
@submitted=${() => {
this.disabled[idx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[idx] = false;
this.requestUpdate();
}, 2000);
}}
>
${["light", "dark"].map(
(slot) => html`
<ha-form
slot=${slot}
.data=${this.data[idx]}
.schema=${info.schema}
.error=${info.error}
.disabled=${this.disabled[idx]}
.computeError=${(error) => translations[error] || error}
.computeLabel=${(schema) =>
translations[schema.name] || schema.name}
@value-changed=${(e) => {
this.data[idx] = e.detail.value;
this.requestUpdate();
}}
></ha-form>
`
)}
</demo-black-white-row>
`;
})}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-form": DemoHaForm;
}
}

View File

@@ -1,122 +0,0 @@
import { html, css, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-card";
const colors = ["#03a9f4", "#ffa600", "#43a047"];
const badges: {
label?: string;
description?: string;
image?: string;
}[] = [
{
label: "label",
},
{
label: "label",
description: "Description",
},
{
description: "Description",
},
{
label: "label",
description: "Description",
image: "/images/living_room.png",
},
{
description: "Description",
image: "/images/living_room.png",
},
{
label: "label",
image: "/images/living_room.png",
},
{
image: "/images/living_room.png",
},
{
label: "big label",
},
{
label: "big label",
description: "Description",
},
{
label: "big label",
description: "Description",
image: "/images/living_room.png",
},
];
@customElement("demo-ha-label-badge")
export class DemoHaLabelBadge extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card>
<div class="card-content">
${badges.map(
(badge) => html`
<ha-label-badge
style="--ha-label-badge-color: ${colors[
Math.floor(Math.random() * colors.length)
]}"
.label=${badge.label}
.description=${badge.description}
.image=${badge.image}
>
</ha-label-badge>
`
)}
</div>
</ha-card>
<ha-card>
<div class="card-content">
${badges.map(
(badge) => html`
<div class="badge">
<ha-label-badge
style="--ha-label-badge-color: ${colors[
Math.floor(Math.random() * colors.length)
]}"
.label=${badge.label}
.description=${badge.description}
.image=${badge.image}
>
</ha-label-badge>
<pre>${JSON.stringify(badge, null, 2)}</pre>
</div>
`
)}
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-left: 16px;
background-color: var(--markdown-code-background-color);
padding: 8px;
}
.badge {
display: flex;
flex-direction: row;
margin-bottom: 16px;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-label-badge": DemoHaLabelBadge;
}
}

View File

@@ -1,131 +0,0 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../src/components/ha-selector/ha-selector";
import "../../../src/components/ha-settings-row";
import { provideHass } from "../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../src/types";
import "../components/demo-black-white-row";
import { BlueprintInput } from "../../../src/data/blueprint";
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
const SCHEMAS: {
name: string;
input: Record<string, BlueprintInput | null>;
}[] = [
{
name: "One of each",
input: {
entity: { name: "Entity", selector: { entity: {} } },
device: { name: "Device", selector: { device: {} } },
addon: { name: "Addon", selector: { addon: {} } },
area: { name: "Area", selector: { area: {} } },
target: { name: "Target", selector: { target: {} } },
number_box: {
name: "Number Box",
selector: {
number: {
min: 0,
max: 10,
mode: "box",
},
},
},
number_slider: {
name: "Number Slider",
selector: {
number: {
min: 0,
max: 10,
mode: "slider",
},
},
},
boolean: { name: "Boolean", selector: { boolean: {} } },
time: { name: "Time", selector: { time: {} } },
action: { name: "Action", selector: { action: {} } },
text: { name: "Text", selector: { text: { multiline: false } } },
text_multiline: {
name: "Text multiline",
selector: { text: { multiline: true } },
},
object: { name: "Object", selector: { object: {} } },
select: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
},
},
];
@customElement("demo-ha-selector")
class DemoHaSelector extends LitElement {
@state() private hass!: HomeAssistant;
private data = SCHEMAS.map(() => ({}));
constructor() {
super();
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
mockEntityRegistry(hass);
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
}
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
const valueChanged = (ev) => {
this.data[idx] = {
...data,
[ev.target.key]: ev.detail.value,
};
this.requestUpdate();
};
return html`
<demo-black-white-row .title=${info.name} .value=${this.data[idx]}>
${["light", "dark"].map((slot) =>
Object.entries(info.input).map(
([key, value]) =>
html`
<ha-settings-row narrow slot=${slot}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
<ha-selector
.hass=${this.hass}
.selector=${value!.selector}
.key=${key}
.value=${data[key] ?? value!.default}
@value-changed=${valueChanged}
></ha-selector>
</ha-settings-row>
`
)
)}
</demo-black-white-row>
`;
})}
`;
}
static styles = css`
paper-input,
ha-selector {
width: 60;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-selector": DemoHaSelector;
}
}

View File

@@ -1,156 +0,0 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
}),
getEntity("switch", "bed_ac", "on", {
friendly_name: "Ecobee",
}),
getEntity("sensor", "bed_temp", "72", {
friendly_name: "Bedroom Temp",
device_class: "temperature",
unit_of_measurement: "°F",
}),
getEntity("light", "living_room_light", "off", {
friendly_name: "Living Room Light",
}),
getEntity("fan", "living_room", "on", {
friendly_name: "Living Room Fan",
}),
getEntity("sensor", "office_humidity", "73", {
friendly_name: "Office Humidity",
device_class: "humidity",
unit_of_measurement: "%",
}),
getEntity("light", "office", "on", {
friendly_name: "Office Light",
}),
getEntity("fan", "kitchen", "on", {
friendly_name: "Second Office Fan",
}),
getEntity("binary_sensor", "kitchen_door", "on", {
friendly_name: "Office Door",
device_class: "door",
}),
];
// TODO: Update image here
const CONFIGS = [
{
heading: "Bedroom",
config: `
- type: area
area: bedroom
image: "/images/bed.png"
`,
},
{
heading: "Living Room",
config: `
- type: area
area: living_room
image: "/images/living_room.png"
`,
},
{
heading: "Office",
config: `
- type: area
area: office
image: "/images/office.jpg"
`,
},
{
heading: "Kitchen",
config: `
- type: area
area: kitchen
image: "/images/kitchen.png"
`,
},
];
@customElement("demo-hui-area-card")
class DemoArea extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
hass.mockWS("config/area_registry/list", () => [
{
name: "Bedroom",
area_id: "bedroom",
},
{
name: "Living Room",
area_id: "living_room",
},
{
name: "Office",
area_id: "office",
},
{
name: "Second Office",
area_id: "kitchen",
},
]);
hass.mockWS("config/device_registry/list", () => []);
hass.mockWS("config/entity_registry/list", () => [
{
area_id: "bedroom",
entity_id: "light.bed_light",
},
{
area_id: "bedroom",
entity_id: "switch.bed_ac",
},
{
area_id: "bedroom",
entity_id: "sensor.bed_temp",
},
{
area_id: "living_room",
entity_id: "light.living_room_light",
},
{
area_id: "living_room",
entity_id: "fan.living_room",
},
{
area_id: "office",
entity_id: "light.office",
},
{
area_id: "office",
entity_id: "sensor.office_humidity",
},
{
area_id: "kitchen",
entity_id: "fan.kitchen",
},
{
area_id: "kitchen",
entity_id: "binary_sensor.kitchen_door",
},
]);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-hui-area-card": DemoArea;
}
}

View File

@@ -187,7 +187,6 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id", device_id: "mock-device-id",
area_id: null, area_id: null,
disabled_by: null, disabled_by: null,
entity_category: null,
entity_id: "binary_sensor.updater", entity_id: "binary_sensor.updater",
name: null, name: null,
icon: null, icon: null,
@@ -212,7 +211,6 @@ const createDeviceRegistryEntries = (
area_id: null, area_id: null,
name_by_user: null, name_by_user: null,
disabled_by: null, disabled_by: null,
configuration_url: null,
}, },
]; ];

View File

@@ -65,11 +65,10 @@ class HaGallery extends PolymerElement {
<app-header slot="header" fixed> <app-header slot="header" fixed>
<app-toolbar> <app-toolbar>
<ha-icon-button <ha-icon-button
icon="hass:arrow-left"
on-click="_backTapped" on-click="_backTapped"
class$="[[_computeHeaderButtonClass(_demo)]]" class$="[[_computeHeaderButtonClass(_demo)]]"
> ></ha-icon-button>
<ha-icon icon="hass:arrow-left"></ha-icon>
</ha-icon-button>
<div main-title> <div main-title>
[[_withDefault(_demo, "Home Assistant Gallery")]] [[_withDefault(_demo, "Home Assistant Gallery")]]
</div> </div>
@@ -176,6 +175,11 @@ class HaGallery extends PolymerElement {
this.addEventListener("alert-dismissed-clicked", () => this.addEventListener("alert-dismissed-clicked", () =>
this.$.notifications.showDialog({ message: "Alert dismissed clicked" }) this.$.notifications.showDialog({ message: "Alert dismissed clicked" })
); );
this.addEventListener("alert-action-clicked", () =>
this.$.notifications.showDialog({ message: "Alert action clicked" })
);
this.addEventListener("hass-more-info", (ev) => { this.addEventListener("hass-more-info", (ev) => {
if (ev.detail.entityId) { if (ev.detail.entityId) {
this.$.notifications.showDialog({ this.$.notifications.showDialog({

View File

@@ -4,7 +4,6 @@ import { property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { import {
HassioAddonInfo, HassioAddonInfo,
@@ -33,7 +32,7 @@ class HassioAddonRepositoryEl extends LitElement {
return filterAndSort(addons, filter); return filterAndSort(addons, filter);
} }
return addons.sort((a, b) => return addons.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name) a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
); );
} }
); );

View File

@@ -1,3 +1,4 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
@@ -17,7 +18,7 @@ import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input"; import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-svg-icon";
import { import {
HassioAddonInfo, HassioAddonInfo,
HassioAddonRepository, HassioAddonRepository,
@@ -25,10 +26,11 @@ import {
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries"; import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
@@ -75,22 +77,24 @@ class HassioAddonStore extends LitElement {
} }
return html` return html`
<hass-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.header=${this.supervisor.localize("panel.store")} .tabs=${supervisorTabs}
main-page
supervisor
> >
<span slot="header"> ${this.supervisor.localize("panel.store")} </span>
<ha-button-menu <ha-button-menu
corner="BOTTOM_START" corner="BOTTOM_START"
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} @action=${this._handleAction}
> >
<ha-icon-button <mwc-icon-button slot="trigger" alt="menu">
.label=${this.supervisor.localize("common.menu")} <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
.path=${mdiDotsVertical} </mwc-icon-button>
slot="trigger"
></ha-icon-button>
<mwc-list-item> <mwc-list-item>
${this.supervisor.localize("store.repositories")} ${this.supervisor.localize("store.repositories")}
</mwc-list-item> </mwc-list-item>
@@ -109,7 +113,6 @@ class HassioAddonStore extends LitElement {
: html` : html`
<div class="search"> <div class="search">
<search-input <search-input
.hass=${this.hass}
no-label-float no-label-float
no-underline no-underline
.filter=${this._filter} .filter=${this._filter}
@@ -128,7 +131,7 @@ class HassioAddonStore extends LitElement {
</div> </div>
` `
: ""} : ""}
</hass-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@@ -15,13 +15,12 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types"; import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor"; import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -78,18 +77,6 @@ class HassioAddonConfig extends LitElement {
this.addon.translations.en?.configuration?.[entry.name].name || this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name; entry.name;
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
);
private _filteredShchema = memoizeOne( private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => (options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required) schema.filter((entry) => entry.name in options || entry.required)
@@ -113,11 +100,9 @@ class HassioAddonConfig extends LitElement {
</h2> </h2>
<div class="card-menu"> <div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button <mwc-icon-button slot="trigger">
.label=${this.hass.localize("common.menu")} <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
.path=${mdiDotsVertical} </mwc-icon-button>
slot="trigger"
></ha-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}> <mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode ${this._yamlMode
? this.supervisor.localize( ? this.supervisor.localize(
@@ -140,14 +125,12 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!} .data=${this._options!}
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
.computeLabel=${this.computeLabel} .computeLabel=${this.computeLabel}
.schema=${this._schema( .schema=${this._showOptional
this._showOptional ? this.addon.schema!
? this.addon.schema! : this._filteredShchema(
: this._filteredShchema( this.addon.options,
this.addon.options, this.addon.schema!
this.addon.schema! )}
)
)}
></ha-form>` ></ha-form>`
: html` <ha-yaml-editor : html` <ha-yaml-editor
@value-changed=${this._configChanged} @value-changed=${this._configChanged}

View File

@@ -108,6 +108,7 @@ class HassioAddonDashboard extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route} .route=${route}
.tabs=${addonTabs} .tabs=${addonTabs}
supervisor supervisor

View File

@@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info"; import "./hassio-addon-info";
@@ -12,8 +12,6 @@ import "./hassio-addon-info";
class HassioAddonInfoDashboard extends LitElement { class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@@ -29,7 +27,6 @@ class HassioAddonInfoDashboard extends LitElement {
<div class="content"> <div class="content">
<hassio-addon-info <hassio-addon-info
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
.addon=${this.addon} .addon=${this.addon}

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { import {
mdiArrowUpBoldCircle,
mdiCheckCircle, mdiCheckCircle,
mdiChip, mdiChip,
mdiCircle, mdiCircle,
@@ -10,12 +11,6 @@ import {
mdiHomeAssistant, mdiHomeAssistant,
mdiKey, mdiKey,
mdiNetwork, mdiNetwork,
mdiNumeric1,
mdiNumeric2,
mdiNumeric3,
mdiNumeric4,
mdiNumeric5,
mdiNumeric6,
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
@@ -30,7 +25,7 @@ import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-chip"; import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
@@ -48,6 +43,7 @@ import {
startHassioAddon, startHassioAddon,
stopHassioAddon, stopHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption, validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { import {
@@ -62,14 +58,14 @@ import {
showConfirmationDialog, showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box"; } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string"; import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
import "../../components/supervisor-metric"; import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "../../update-available/update-available-card"; import { addonArchIsSupported } from "../../util/addon";
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
const STAGE_ICON = { const STAGE_ICON = {
stable: mdiCheckCircle, stable: mdiCheckCircle,
@@ -77,21 +73,10 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick, deprecated: mdiExclamationThick,
}; };
const RATING_ICON = {
1: mdiNumeric1,
2: mdiNumeric2,
3: mdiNumeric3,
4: mdiNumeric4,
5: mdiNumeric5,
6: mdiNumeric6,
};
@customElement("hassio-addon-info") @customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement { class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails; @property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -128,35 +113,91 @@ class HassioAddonInfo extends LitElement {
return html` return html`
${this.addon.update_available ${this.addon.update_available
? html` ? html`
<update-available-card <ha-card
.hass=${this.hass} .header="${this.supervisor.localize(
.narrow=${this.narrow} "common.update_available",
.supervisor=${this.supervisor} "count",
.addonSlug=${this.addon.slug} 1
></update-available-card> )}🎉"
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${this.supervisor.localize(
"addon.dashboard.new_update_available",
"name",
this.addon.name,
"version",
this.addon.version_latest
)}
.description=${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available && addonStoreInfo
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</ha-alert>
`
: html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
</ha-alert>
`
: ""}
</div>
<div class="card-actions">
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</mwc-button>
`
: html`<span></span>`}
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button>
</div>
</ha-card>
` `
: ""} : ""}
${!this.addon.protected ${!this.addon.protected
? html` ? html`
<ha-alert <ha-card class="warning">
alert-type="error" <h1 class="card-header">${this.supervisor.localize(
.title=${this.supervisor.localize( "addon.dashboard.protection_mode.title"
"addon.dashboard.protection_mode.title" )}
)} </h1>
> <div class="card-content">
${this.supervisor.localize("addon.dashboard.protection_mode.content")}
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize( ${this.supervisor.localize(
"addon.dashboard.protection_mode.content" "addon.dashboard.protection_mode.enable"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@click=${this._protectionToggled}
>
</mwc-button> </mwc-button>
</ha-alert> </div>
` </div>
</ha-card>
`
: ""} : ""}
<ha-card> <ha-card>
@@ -208,163 +249,6 @@ class HassioAddonInfo extends LitElement {
>`} >`}
</div> </div>
<div class="capabilities">
${this.addon.stage !== "stable"
? html` <ha-chip
hasIcon
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
>
<ha-svg-icon
slot="icon"
.path=${STAGE_ICON[this.addon.stage]}
>
</ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.stages.${this.addon.stage}`
)}
</ha-chip>`
: ""}
<ha-chip
hasIcon
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
>
<ha-svg-icon slot="icon" .path=${RATING_ICON[this.addon.rating]}>
</ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.rating"
)}
</ha-chip>
${this.addon.host_network
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="host_network"
>
<ha-svg-icon slot="icon" .path=${mdiNetwork}> </ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
</ha-chip>
`
: ""}
${this.addon.full_access
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="full_access"
>
<ha-svg-icon slot="icon" .path=${mdiChip}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
</ha-chip>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
id="homeassistant_api"
>
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.core"
)}
</ha-chip>
`
: ""}
${this._computeHassioApi
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="hassio_api">
<ha-svg-icon
slot="icon"
.path=${mdiHomeAssistant}
></ha-svg-icon>
${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
</ha-chip>
`
: ""}
${this.addon.docker_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="docker_api">
<ha-svg-icon slot="icon" .path=${mdiDocker}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}
</ha-chip>
`
: ""}
${this.addon.host_pid
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="host_pid">
<ha-svg-icon slot="icon" .path=${mdiPound}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
</ha-chip>
`
: ""}
${this.addon.apparmor !== "default"
? html`
<ha-chip
hasIcon
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
>
<ha-svg-icon slot="icon" .path=${mdiShield}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
</ha-chip>
`
: ""}
${this.addon.auth_api
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="auth_api">
<ha-svg-icon slot="icon" .path=${mdiKey}></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
</ha-chip>
`
: ""}
${this.addon.ingress
? html`
<ha-chip hasIcon @click=${this._showMoreInfo} id="ingress">
<ha-svg-icon
slot="icon"
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
</ha-chip>
`
: ""}
</div>
<div class="description light-color"> <div class="description light-color">
${this.addon.description}.<br /> ${this.addon.description}.<br />
${this.supervisor.localize( ${this.supervisor.localize(
@@ -385,6 +269,171 @@ class HassioAddonInfo extends LitElement {
/> />
` `
: ""} : ""}
<div class="security">
${this.addon.stage !== "stable"
? html` <ha-label-badge
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
description=""
>
<ha-svg-icon
.path=${STAGE_ICON[this.addon.stage]}
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
.value=${this.addon.rating}
label="rating"
description=""
></ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.full_access
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this._computeHassioApi
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.docker_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.host_pid
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.apparmor
? html`
<ha-label-badge
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.auth_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
</ha-label-badge>
`
: ""}
${this.addon.ingress
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
description=""
>
<ha-svg-icon
.path=${mdiCursorDefaultClickOutline}
></ha-svg-icon>
</ha-label-badge>
`
: ""}
</div>
${this.addon.version ${this.addon.version
? html` ? html`
<div <div
@@ -846,14 +895,22 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
try { try {
const content = await fetchHassioAddonChangelog( let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug);
this.hass, if (
this.addon.slug content.includes(`# ${this.addon.version}`) &&
); content.includes(`# ${this.addon.version_latest}`)
) {
const newcontent = content.split(`# ${this.addon.version}`)[0];
if (newcontent.includes(`# ${this.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;
}
}
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content: extractChangelog(this.addon, content), content,
}); });
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -928,6 +985,33 @@ class HassioAddonInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _updateClicked(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: this.addon.name,
version: this.addon.version_latest,
backupParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
},
updateHandler: async () => this._updateAddon(),
});
}
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
}
private async _startClicked(ev: CustomEvent): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
@@ -1093,31 +1177,34 @@ class HassioAddonInfo extends LitElement {
.description a { .description a {
color: var(--primary-color); color: var(--primary-color);
} }
ha-chip {
text-transform: capitalize;
--ha-chip-text-color: var(--text-primary-color);
--ha-chip-background-color: var(--primary-color);
}
.red { .red {
--ha-chip-background-color: var(--label-badge-red, #df4c1e); --ha-label-badge-color: var(--label-badge-red, #df4c1e);
} }
.blue { .blue {
--ha-chip-background-color: var(--label-badge-blue, #039be5); --ha-label-badge-color: var(--label-badge-blue, #039be5);
} }
.green { .green {
--ha-chip-background-color: var(--label-badge-green, #0da035); --ha-label-badge-color: var(--label-badge-green, #0da035);
} }
.yellow { .yellow {
--ha-chip-background-color: var(--label-badge-yellow, #f4b400); --ha-label-badge-color: var(--label-badge-yellow, #f4b400);
} }
.capabilities { .security {
margin-bottom: 16px; margin-bottom: 16px;
} }
.card-actions { .card-actions {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
} }
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--ha-label-badge-padding: 8px 0 0 0;
}
.changelog { .changelog {
display: contents; display: contents;
} }
@@ -1156,21 +1243,7 @@ class HassioAddonInfo extends LitElement {
align-self: end; align-self: end;
} }
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
update-available-card {
padding-bottom: 16px;
}
@media (max-width: 720px) { @media (max-width: 720px) {
ha-chip {
line-height: 36px;
}
.addon-options { .addon-options {
max-width: 100%; max-width: 100%;
} }

View File

@@ -23,8 +23,7 @@ import {
} from "../../../src/components/data-table/ha-data-table"; } from "../../../src/components/data-table/ha-data-table";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-fab"; import "../../../src/components/ha-fab";
import "../../../src/components/ha-icon-button"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/components/ha-svg-icon";
import { import {
fetchHassioBackups, fetchHassioBackups,
friendlyFolderName, friendlyFolderName,
@@ -32,7 +31,6 @@ import {
reloadHassioBackups, reloadHassioBackups,
removeBackup, removeBackup,
} from "../../../src/data/hassio/backup"; } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -42,9 +40,9 @@ import "../../../src/layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../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 { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup"; import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup";
import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@@ -158,7 +156,7 @@ export class HassioBackups extends LitElement {
} }
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.tabs=${supervisorTabs(this.hass)} .tabs=${supervisorTabs}
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")} .searchLabel=${this.supervisor.localize("search")}
@@ -181,11 +179,9 @@ export class HassioBackups extends LitElement {
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} @action=${this._handleAction}
> >
<ha-icon-button <mwc-icon-button slot="trigger" alt="menu">
.label=${this.hass.localize("common.menu")} <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
.path=${mdiDotsVertical} </mwc-icon-button>
slot="trigger"
></ha-icon-button>
<mwc-list-item> <mwc-list-item>
${this.supervisor?.localize("common.reload")} ${this.supervisor?.localize("common.reload")}
</mwc-list-item> </mwc-list-item>
@@ -220,15 +216,13 @@ export class HassioBackups extends LitElement {
</mwc-button> </mwc-button>
` `
: html` : html`
<ha-icon-button <mwc-icon-button
.label=${this.supervisor.localize(
"snapshot.delete_selected"
)}
.path=${mdiDelete}
id="delete-btn" id="delete-btn"
class="warning" class="warning"
@click=${this._deleteSelected} @click=${this._deleteSelected}
></ha-icon-button> >
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<paper-tooltip animation-delay="0" for="delete-btn"> <paper-tooltip animation-delay="0" for="delete-btn">
${this.supervisor.localize("backup.delete_selected")} ${this.supervisor.localize("backup.delete_selected")}
</paper-tooltip> </paper-tooltip>
@@ -374,7 +368,7 @@ export class HassioBackups extends LitElement {
margin-right: -12px; margin-right: -12px;
} }
.header-btns > mwc-button, .header-btns > mwc-button,
.header-btns > ha-icon-button { .header-btns > mwc-icon-button {
margin: 8px; margin: 8px;
} }
`, `,

View File

@@ -1,6 +1,7 @@
import { mdiHelpCircle } from "@mdi/js"; import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-relative-time";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
@@ -18,6 +19,8 @@ class HassioCardContent extends LitElement {
@property() public topbarClass?: string; @property() public topbarClass?: string;
@property() public datetime?: string;
@property() public iconTitle?: string; @property() public iconTitle?: string;
@property() public iconClass?: string; @property() public iconClass?: string;
@@ -53,6 +56,15 @@ class HassioCardContent extends LitElement {
/* treat as available when undefined */ /* treat as available when undefined */
this.available === false ? " (Not available)" : "" this.available === false ? " (Not available)" : ""
} }
${this.datetime
? html`
<ha-relative-time
.hass=${this.hass}
class="addition"
.datetime=${this.datetime}
></ha-relative-time>
`
: undefined}
</div> </div>
</div> </div>
`; `;
@@ -94,6 +106,9 @@ class HassioCardContent extends LitElement {
height: 2.4em; height: 2.4em;
line-height: 1.2em; line-height: 1.2em;
} }
ha-relative-time {
display: block;
}
.icon_image img { .icon_image img {
max-height: 40px; max-height: 40px;
max-width: 40px; max-width: 40px;

View File

@@ -1,3 +1,4 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js"; import { mdiFolderUpload } from "@mdi/js";
import "@polymer/paper-input/paper-input-container"; import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
@@ -5,8 +6,9 @@ import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload"; import "../../../src/components/ha-file-upload";
import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup"; import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
@@ -16,9 +18,11 @@ declare global {
} }
} }
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
@customElement("hassio-upload-backup") @customElement("hassio-upload-backup")
export class HassioUploadBackup extends LitElement { export class HassioUploadBackup extends LitElement {
public hass?: HomeAssistant; public hass!: HomeAssistant;
@state() public value: string | null = null; @state() public value: string | null = null;
@@ -27,7 +31,6 @@ export class HassioUploadBackup extends LitElement {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<ha-file-upload <ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading} .uploading=${this._uploading}
.icon=${mdiFolderUpload} .icon=${mdiFolderUpload}
accept="application/x-tar" accept="application/x-tar"
@@ -41,6 +44,20 @@ export class HassioUploadBackup extends LitElement {
private async _uploadFile(ev) { private async _uploadFile(ev) {
const file = ev.detail.files[0]; const file = ev.detail.files[0];
if (file.size > MAX_FILE_SIZE) {
showAlertDialog(this, {
title: "Backup file is too big",
text: html`The maximum allowed filesize is 1GB.<br />
<a
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-backup-on-a-new-install"
target="_blank"
>Have a look here on how to restore it.</a
>`,
confirmText: "ok",
});
return;
}
if (!["application/x-tar"].includes(file.type)) { if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Unsupported file format", title: "Unsupported file format",

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare"; import { stringCompare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -20,9 +20,7 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="content"> <div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12) <h1>${this.supervisor.localize("dashboard.addons")}</h1>
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
: ""}
<div class="card-group"> <div class="card-group">
${!this.supervisor.supervisor.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`
@@ -35,7 +33,7 @@ class HassioAddons extends LitElement {
</ha-card> </ha-card>
` `
: this.supervisor.supervisor.addons : this.supervisor.supervisor.addons
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) .sort((a, b) => stringCompare(a.name, b.name))
.map( .map(
(addon) => html` (addon) => html`
<ha-card .addon=${addon} @click=${this._addonTapped}> <ha-card .addon=${addon} @click=${this._addonTapped}>

View File

@@ -1,8 +1,5 @@
import { mdiStorePlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/ha-fab";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -28,37 +25,23 @@ class HassioDashboard extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs(this.hass)} .tabs=${supervisorTabs}
main-page main-page
supervisor supervisor
hasFab
> >
<span slot="header"> <span slot="header">
${this.supervisor.localize("panel.dashboard")} ${this.supervisor.localize("panel.dashboard")}
</span> </span>
<div class="content"> <div class="content">
${this.hass.config.version.includes("dev") || <hassio-update
!atLeastVersion(this.hass.config.version, 2021, 12) .hass=${this.hass}
? html` .supervisor=${this.supervisor}
<hassio-update ></hassio-update>
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
`
: ""}
<hassio-addons <hassio-addons
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
></hassio-addons> ></hassio-addons>
</div> </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> </hass-tabs-subpage>
`; `;
} }

View File

@@ -3,18 +3,34 @@ import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; 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/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import { import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { updateCore } from "../../../src/data/supervisor/core";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => const computeVersion = (key: string, version: string): string =>
@@ -57,18 +73,26 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
"core", "core",
this.supervisor.core this.supervisor.core,
"hassio/homeassistant/update",
`https://${
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
)} )}
${this._renderUpdateCard( ${this._renderUpdateCard(
"Supervisor", "Supervisor",
"supervisor", "supervisor",
this.supervisor.supervisor this.supervisor.supervisor,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
)} )}
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
"os", "os",
this.supervisor.os this.supervisor.os,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
) )
: ""} : ""}
</div> </div>
@@ -79,7 +103,9 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard( private _renderUpdateCard(
name: string, name: string,
key: string, key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string
): TemplateResult { ): TemplateResult {
if (!object.update_available) { if (!object.update_available) {
return html``; return html``;
@@ -110,15 +136,96 @@ export class HassioUpdate extends LitElement {
</ha-settings-row> </ha-settings-row>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href="/hassio/update-available/${key}"> <a href=${releaseNotesUrl} target="_blank" rel="noreferrer">
<mwc-button .label=${this.supervisor.localize("common.show")}> <mwc-button>
${this.supervisor.localize("common.release_notes")}
</mwc-button> </mwc-button>
</a> </a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
item.progress = false;
return;
}
try {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err: any) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
});
}
}
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles"; import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -15,7 +14,7 @@ export class DialogHassioBackupUpload
extends LitElement extends LitElement
implements HassDialog<HassioBackupUploadDialogParams> implements HassDialog<HassioBackupUploadDialogParams>
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: HassioBackupUploadDialogParams; @state() private _params?: HassioBackupUploadDialogParams;
@@ -53,12 +52,9 @@ export class DialogHassioBackupUpload
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title"> Upload backup </span> <span slot="title"> Upload backup </span>
<ha-icon-button <mwc-icon-button slot="actionItems" dialogAction="cancel">
.label=${this.hass?.localize("common.close") || "close"} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
.path=${mdiClose} </mwc-icon-button>
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
<hassio-upload-backup <hassio-upload-backup

View File

@@ -9,7 +9,7 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu"; import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
@@ -35,7 +35,7 @@ class HassioBackupDialog
extends LitElement extends LitElement
implements HassDialog<HassioBackupDialogParams> implements HassDialog<HassioBackupDialogParams>
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: string; @state() private _error?: string;
@@ -76,12 +76,9 @@ class HassioBackupDialog
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title">${this._backup.name}</span> <span slot="title">${this._backup.name}</span>
<ha-icon-button <mwc-icon-button slot="actionItems" dialogAction="cancel">
.label=${this.hass?.localize("common.close") || "close"} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
.path=${mdiClose} </mwc-icon-button>
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
</div> </div>
${this._restoringBackup ${this._restoringBackup
@@ -113,11 +110,9 @@ class HassioBackupDialog
@action=${this._handleMenuAction} @action=${this._handleMenuAction}
@closed=${stopPropagation} @closed=${stopPropagation}
> >
<ha-icon-button <mwc-icon-button slot="trigger" alt="menu">
.label=${this.hass!.localize("common.menu")} <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
.path=${mdiDotsVertical} </mwc-icon-button>
slot="trigger"
></ha-icon-button>
<mwc-list-item>Download Backup</mwc-list-item> <mwc-list-item>Download Backup</mwc-list-item>
<mwc-list-item class="error">Delete Backup</mwc-list-item> <mwc-list-item class="error">Delete Backup</mwc-list-item>
</ha-button-menu>` </ha-button-menu>`
@@ -131,6 +126,9 @@ class HassioBackupDialog
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-svg-icon {
color: var(--primary-text-color);
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
text-align: center; text-align: center;
@@ -141,9 +139,6 @@ class HassioBackupDialog
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;
} }
ha-icon-button {
color: var(--secondary-text-color);
}
`, `,
]; ];
} }
@@ -192,23 +187,25 @@ class HassioBackupDialog
} }
if (!this._dialogParams?.onboarding) { if (!this._dialogParams?.onboarding) {
this.hass!.callApi( this.hass
"POST", .callApi(
"POST",
`hassio/${ `hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9) atLeastVersion(this.hass.config.version, 2021, 9)
? "backups" ? "backups"
: "snapshots" : "snapshots"
}/${this._backup!.slug}/restore/partial`, }/${this._backup!.slug}/restore/partial`,
backupDetails backupDetails
).then( )
() => { .then(
this.closeDialog(); () => {
}, this.closeDialog();
(error) => { },
this._error = error.body.message; (error) => {
} this._error = error.body.message;
); }
);
} else { } else {
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, { fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
@@ -242,22 +239,24 @@ class HassioBackupDialog
} }
if (!this._dialogParams?.onboarding) { if (!this._dialogParams?.onboarding) {
this.hass!.callApi( this.hass
"POST", .callApi(
`hassio/${ "POST",
atLeastVersion(this.hass!.config.version, 2021, 9) `hassio/${
? "backups" atLeastVersion(this.hass.config.version, 2021, 9)
: "snapshots" ? "backups"
}/${this._backup!.slug}/restore/full`, : "snapshots"
backupDetails }/${this._backup!.slug}/restore/full`,
).then( backupDetails
() => { )
this.closeDialog(); .then(
}, () => {
(error) => { this.closeDialog();
this._error = error.body.message; },
} (error) => {
); this._error = error.body.message;
}
);
} else { } else {
fireEvent(this, "restoring"); fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, { fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
@@ -279,33 +278,36 @@ class HassioBackupDialog
return; return;
} }
this.hass!.callApi( this.hass
atLeastVersion(this.hass!.config.version, 2021, 9) ? "DELETE" : "POST",
`hassio/${ .callApi(
atLeastVersion(this.hass!.config.version, 2021, 9) atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST",
? `backups/${this._backup!.slug}` `hassio/${
: `snapshots/${this._backup!.slug}/remove` atLeastVersion(this.hass.config.version, 2021, 9)
}` ? `backups/${this._backup!.slug}`
).then( : `snapshots/${this._backup!.slug}/remove`
() => { }`
if (this._dialogParams!.onDelete) { )
this._dialogParams!.onDelete(); .then(
() => {
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
this.closeDialog();
},
(error) => {
this._error = error.body.message;
} }
this.closeDialog(); );
},
(error) => {
this._error = error.body.message;
}
);
} }
private async _downloadClicked() { private async _downloadClicked() {
let signedPath: { path: string }; let signedPath: { path: string };
try { try {
signedPath = await getSignedPath( signedPath = await getSignedPath(
this.hass!, this.hass,
`/api/hassio/${ `/api/hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9) atLeastVersion(this.hass.config.version, 2021, 9)
? "backups" ? "backups"
: "snapshots" : "snapshots"
}/${this._backup!.slug}/download` }/${this._backup!.slug}/download`

View File

@@ -60,72 +60,85 @@ class HassioDatadiskDialog extends LitElement {
if (!this.dialogParams) { if (!this.dialogParams) {
return html``; return html``;
} }
let heading: string;
let content: TemplateResult;
if (this.moving) {
heading = this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving"
);
content = html`
<ha-circular-progress alt="Moving" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.moving_desc"
)}
</p>
`;
} else {
heading = this.dialogParams.supervisor.localize(
"dialog.datadisk_move.title"
);
content = 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 />
<paper-dropdown-menu
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@value-changed=${this._select_device}
>
<paper-listbox slot="dropdown-content">
${this.devices.map(
(device) => html`<paper-item>${device}</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
"dialog.datadisk_move.loading_devices"
)
: this.dialogParams.supervisor.localize(
"dialog.datadisk_move.no_devices"
)}
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
</mwc-button>
<mwc-button
.disabled=${!this.selectedDevice}
slot="primaryAction"
@click=${this._moveDatadisk}
>
${this.dialogParams.supervisor.localize("dialog.datadisk_move.move")}
</mwc-button>
`;
}
return html` return html`
<ha-dialog <ha-dialog
open open
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
.heading=${this.moving
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
@closed=${this.closeDialog} @closed=${this.closeDialog}
?hideActions=${this.moving} ?hideActions=${this.moving}
.heading=${heading}
> >
${this.moving ${content}
? html` <ha-circular-progress alt="Moving" size="large" active>
</ha-circular-progress>
<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 />
<paper-dropdown-menu
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@value-changed=${this._select_device}
>
<paper-listbox slot="dropdown-content">
${this.devices.map(
(device) => html`<paper-item>${device}</paper-item>`
)}
</paper-listbox>
</paper-dropdown-menu>
`
: this.devices === undefined
? this.dialogParams.supervisor.localize(
"dialog.datadisk_move.loading_devices"
)
: this.dialogParams.supervisor.localize(
"dialog.datadisk_move.no_devices"
)}
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.cancel"
)}
</mwc-button>
<mwc-button
.disabled=${!this.selectedDevice}
slot="primaryAction"
@click=${this._moveDatadisk}
>
${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.move"
)}
</mwc-button>`}
</ha-dialog> </ha-dialog>
`; `;
} }

View File

@@ -7,7 +7,6 @@ import "../../../../src/common/search/search-input";
import { stringCompare } from "../../../../src/common/string/compare"; import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-icon-button";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump"; import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
@@ -71,13 +70,10 @@ class HassioHardwareDialog extends LitElement {
<h2> <h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")} ${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2> </h2>
<ha-icon-button <mwc-icon-button dialogAction="close">
.label=${this.hass.localize("common.close")} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
.path=${mdiClose} </mwc-icon-button>
dialogAction="close"
></ha-icon-button>
<search-input <search-input
.hass=${this.hass}
autofocus autofocus
no-label-float no-label-float
.filter=${this._filter} .filter=${this._filter}
@@ -145,7 +141,7 @@ class HassioHardwareDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
ha-icon-button { mwc-icon-button {
position: absolute; position: absolute;
right: 16px; right: 16px;
top: 10px; top: 10px;

View File

@@ -47,6 +47,11 @@ class HassioMarkdownDialog extends LitElement {
haStyleDialog, haStyleDialog,
hassioStyle, hassioStyle,
css` css`
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
border-radius: 2px;
}
app-toolbar { app-toolbar {
margin: 0; margin: 0;
padding: 0 16px; padding: 0 16px;
@@ -57,6 +62,19 @@ class HassioMarkdownDialog extends LitElement {
margin-left: 16px; margin-left: 16px;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-paper-dialog {
max-height: 100%;
}
ha-paper-dialog::before {
content: "";
position: fixed;
z-index: -1;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: inherit;
}
app-toolbar { app-toolbar {
color: var(--text-primary-color); color: var(--text-primary-color);
background-color: var(--primary-color); background-color: var(--primary-color);

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
@@ -15,9 +16,9 @@ import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
AccessPoints, AccessPoints,
@@ -103,12 +104,9 @@ export class DialogHassioNetwork
<span slot="title"> <span slot="title">
${this.supervisor.localize("dialog.network.title")} ${this.supervisor.localize("dialog.network.title")}
</span> </span>
<ha-icon-button <mwc-icon-button slot="actionItems" dialogAction="cancel">
.label=${this.hass.localize("common.close")} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
.path=${mdiClose} </mwc-icon-button>
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar> </ha-header-bar>
${this._interfaces.length > 1 ${this._interfaces.length > 1
? html`<mwc-tab-bar ? html`<mwc-tab-bar

View File

@@ -1,12 +1,13 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js"; import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form"; import "../../../../src/components/ha-svg-icon";
import { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
addHassioDockerRegistry, addHassioDockerRegistry,
@@ -19,41 +20,22 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries"; import { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
{
type: "string",
name: "registry",
required: true,
},
{
type: "string",
name: "username",
required: true,
},
{
type: "string",
name: "password",
required: true,
format: "password",
},
];
@customElement("dialog-hassio-registries") @customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement { class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@state() private _registries?: { @property({ attribute: false }) private _registries?: {
registry: string; registry: string;
username: string; username: string;
}[]; }[];
@state() private _input: { @state() private _registry?: string;
registry?: string;
username?: string; @state() private _username?: string;
password?: string;
} = {}; @state() private _password?: string;
@state() private _opened = false; @state() private _opened = false;
@@ -66,7 +48,6 @@ class HassioRegistriesDialog extends LitElement {
@closed=${this.closeDialog} @closed=${this.closeDialog}
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this._addingRegistry this._addingRegistry
@@ -74,77 +55,100 @@ class HassioRegistriesDialog extends LitElement {
: this.supervisor.localize("dialog.registries.title_manage") : this.supervisor.localize("dialog.registries.title_manage")
)} )}
> >
${this._addingRegistry <div class="form">
? html` ${this._addingRegistry
<ha-form ? html`
.data=${this._input} <paper-input
.schema=${SCHEMA} @value-changed=${this._inputChanged}
@value-changed=${this._valueChanged} class="flex-auto"
.computeLabel=${this._computeLabel} name="registry"
></ha-form> .label=${this.supervisor.localize(
<div class="action"> "dialog.registries.registry"
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
type="password"
required
auto-validate
></paper-input>
<mwc-button <mwc-button
?disabled=${Boolean( ?disabled=${Boolean(
!this._input.registry || !this._registry || !this._username || !this._password
!this._input.username ||
!this._input.password
)} )}
@click=${this._addNewRegistry} @click=${this._addNewRegistry}
> >
${this.supervisor.localize("dialog.registries.add_registry")} ${this.supervisor.localize("dialog.registries.add_registry")}
</mwc-button> </mwc-button>
</div> `
` : html`${this._registries?.length
: html`${this._registries?.length ? this._registries.map(
? this._registries.map( (entry) => html`
(entry) => html` <mwc-list-item class="option" hasMeta twoline>
<ha-settings-row class="registry"> <span>${entry.registry}</span>
<span slot="heading"> ${entry.registry} </span> <span slot="secondary"
<span slot="description"> >${this.supervisor.localize(
${this.supervisor.localize( "dialog.registries.username"
"dialog.registries.username" )}:
)}: ${entry.username}</span
${entry.username} >
</span> <mwc-icon-button
<ha-icon-button .entry=${entry}
.entry=${entry} .title=${this.supervisor.localize(
.label=${this.supervisor.localize( "dialog.registries.remove"
"dialog.registries.remove" )}
)} slot="meta"
.path=${mdiDelete} @click=${this._removeRegistry}
@click=${this._removeRegistry} >
></ha-icon-button> <ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</ha-settings-row> </mwc-icon-button>
` </mwc-list-item>
) `
: html` )
<ha-alert> : html`
${this.supervisor.localize( <mwc-list-item>
"dialog.registries.no_registries" <span
)} >${this.supervisor.localize(
</ha-alert> "dialog.registries.no_registries"
`} )}</span
<div class="action"> >
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}> <mwc-button @click=${this._addRegistry}>
${this.supervisor.localize( ${this.supervisor.localize(
"dialog.registries.add_new_registry" "dialog.registries.add_new_registry"
)} )}
</mwc-button> </mwc-button> `}
</div> `} </div>
</ha-dialog> </ha-dialog>
`; `;
} }
private _computeLabel = (schema: HaFormSchema) => private _inputChanged(ev: Event) {
this.supervisor.localize(`dialog.registries.${schema.name}`) || schema.name; const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;
} }
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> { public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true; this._opened = true;
this._input = {};
this.supervisor = dialogParams.supervisor; this.supervisor = dialogParams.supervisor;
await this._loadRegistries(); await this._loadRegistries();
await this.updateComplete; await this.updateComplete;
@@ -153,7 +157,6 @@ class HassioRegistriesDialog extends LitElement {
public closeDialog(): void { public closeDialog(): void {
this._addingRegistry = false; this._addingRegistry = false;
this._opened = false; this._opened = false;
this._input = {};
} }
public focus(): void { public focus(): void {
@@ -178,16 +181,15 @@ class HassioRegistriesDialog extends LitElement {
private async _addNewRegistry(): Promise<void> { private async _addNewRegistry(): Promise<void> {
const data = {}; const data = {};
data[this._input.registry!] = { data[this._registry!] = {
username: this._input.username, username: this._username,
password: this._input.password, password: this._password,
}; };
try { try {
await addHassioDockerRegistry(this.hass, data); await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries(); await this._loadRegistries();
this._addingRegistry = false; this._addingRegistry = false;
this._input = {};
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_add"), title: this.supervisor.localize("dialog.registries.failed_to_add"),
@@ -215,20 +217,32 @@ class HassioRegistriesDialog extends LitElement {
haStyle, haStyle,
haStyleDialog, haStyleDialog,
css` css`
.registry { ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
border-radius: 4px; border-radius: 4px;
margin-top: 4px; margin-top: 4px;
} }
.action { mwc-button {
margin-top: 24px; margin-left: 8px;
width: 100%;
display: flex;
justify-content: flex-end;
} }
ha-icon-button { mwc-icon-button {
color: var(--error-color); color: var(--error-color);
margin-right: -10px; margin: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
} }
`, `,
]; ];

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiDelete } from "@mdi/js"; import { mdiDelete } from "@mdi/js";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
@@ -8,11 +9,10 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-svg-icon";
import { import {
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
HassioAddonRepository, HassioAddonRepository,
@@ -57,7 +57,7 @@ class HassioRepositoriesDialog extends LitElement {
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) => private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
repos repos
.filter((repo) => repo.slug !== "core" && repo.slug !== "local") .filter((repo) => repo.slug !== "core" && repo.slug !== "local")
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) .sort((a, b) => (a.name < b.name ? -1 : 1))
); );
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -89,14 +89,15 @@ class HassioRepositoriesDialog extends LitElement {
<div secondary>${repo.maintainer}</div> <div secondary>${repo.maintainer}</div>
<div secondary>${repo.url}</div> <div secondary>${repo.url}</div>
</paper-item-body> </paper-item-body>
<ha-icon-button <mwc-icon-button
.slug=${repo.slug} .slug=${repo.slug}
.label=${this._dialogParams!.supervisor.localize( .title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove" "dialog.repositories.remove"
)} )}
.path=${mdiDelete}
@click=${this._removeRepository} @click=${this._removeRepository}
></ha-icon-button> >
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</paper-item> </paper-item>
` `
) )

View File

@@ -0,0 +1,204 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialBackup } from "../../../../src/data/hassio/backup";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
@state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createBackup = true;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
${this._dialogParams.supervisor.localize(
"dialog.update.backup"
)}
</span>
<span slot="description">
${this._dialogParams.supervisor.localize(
"dialog.update.create_backup",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createBackup}
haptic
@click=${this._toggleBackup}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.creating_backup",
"name",
this._dialogParams.name
)}
</p>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
</ha-dialog>
`;
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
this._action = "backup";
try {
await createHassioPartialBackup(
this.hass,
this._dialogParams!.backupParams
);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
}
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-update": DialogSupervisorUpdate;
}
}

View File

@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
backupParams: any;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
dialogImport: () => import("./dialog-supervisor-update"),
dialogParams,
});
};

View File

@@ -10,7 +10,7 @@ import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen"; import "../../src/layouts/hass-loading-screen";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import "./hassio-router"; import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element"; import { SupervisorBaseElement } from "./supervisor-base-element";
@@ -24,6 +24,8 @@ export class HassioMain extends SupervisorBaseElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route?: Route;
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
@@ -111,6 +113,12 @@ export class HassioMain extends SupervisorBaseElement {
: this.hass.themes.default_theme); : this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme; themeSettings = this.hass.selectedTheme;
if (themeSettings?.dark === undefined) {
themeSettings = {
...this.hass.selectedTheme,
dark: this.hass.themes.darkMode,
};
}
} else { } else {
themeName = themeName =
(this.hass.selectedTheme as unknown as string) || (this.hass.selectedTheme as unknown as string) ||

View File

@@ -34,9 +34,6 @@ const REDIRECTS: Redirects = {
supervisor_store: { supervisor_store: {
redirect: "/hassio/store", redirect: "/hassio/store",
}, },
supervisor_addons: {
redirect: "/hassio/dashboard",
},
supervisor_addon: { supervisor_addon: {
redirect: "/hassio/addon", redirect: "/hassio/addon",
params: { params: {

View File

@@ -35,10 +35,6 @@ class HassioRouter extends HassRouterPage {
backups: "dashboard", backups: "dashboard",
store: "dashboard", store: "dashboard",
system: "dashboard", system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: { addon: {
tag: "hassio-addon-dashboard", tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"), load: () => import("./addon-view/hassio-addon-dashboard"),

View File

@@ -1,22 +1,16 @@
import { import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js";
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 { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [ export const supervisorTabs: PageNavigation[] = [
{ {
translationKey: atLeastVersion(hass.config.version, 2021, 12) translationKey: "panel.dashboard",
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`, path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12) iconPath: mdiViewDashboard,
? mdiPuzzle },
: mdiViewDashboard, {
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
}, },
{ {
translationKey: "panel.backups", translationKey: "panel.backups",

View File

@@ -12,7 +12,6 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params"; import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status"; import { nextRender } from "../../../src/common/util/render-status";
import "../../../src/components/ha-icon-button";
import { import {
fetchHassioAddonInfo, fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
@@ -73,11 +72,12 @@ class HassioIngressView extends LitElement {
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header"> ? html`<div class="header">
<ha-icon-button <mwc-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")} aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu} @click=${this._toggleMenu}
></ha-icon-button> >
<ha-svg-icon .path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
<div class="main-title">${this._addon.name}</div> <div class="main-title">${this._addon.name}</div>
</div> </div>
${iframe}` ${iframe}`
@@ -241,7 +241,7 @@ class HassioIngressView extends LitElement {
flex-grow: 1; flex-grow: 1;
} }
ha-icon-button { mwc-icon-button {
pointer-events: auto; pointer-events: auto;
} }

View File

@@ -25,7 +25,7 @@ import {
} from "../../src/data/supervisor/supervisor"; } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation"; import { getTranslation } from "../../src/util/common-translation";
declare global { declare global {
@@ -38,8 +38,6 @@ declare global {
export class SupervisorBaseElement extends urlSyncMixin( export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement) ProvideHassLitMixin(LitElement)
) { ) {
@property({ attribute: false }) public route?: Route;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = { @property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "", localize: () => "",
}; };
@@ -110,9 +108,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._language = this.hass.language; this._language = this.hass.language;
} }
this._initializeLocalize(); this._initializeLocalize();
if (this.route?.prefix === "/hassio") { this._initSupervisor();
this._initSupervisor();
}
} }
private async _initializeLocalize() { private async _initializeLocalize() {

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; 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/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -12,7 +12,7 @@ import {
fetchHassioStats, fetchHassioStats,
HassioStats, HassioStats,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { restartCore } from "../../../src/data/supervisor/core"; import { restartCore, updateCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -22,6 +22,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info") @customElement("hassio-core-info")
@@ -66,15 +67,14 @@ class HassioCoreInfo extends LitElement {
<span slot="description"> <span slot="description">
core-${this.supervisor.core.version_latest} core-${this.supervisor.core.version_latest}
</span> </span>
${!atLeastVersion(this.hass.config.version, 2021, 12) && ${this.supervisor.core.update_available
this.supervisor.core.update_available
? html` ? html`
<a href="/hassio/update-available/core"> <ha-progress-button
<mwc-button .title=${this.supervisor.localize("common.update")}
.label=${this.supervisor.localize("common.show")} @click=${this._coreUpdate}
> >
</mwc-button> ${this.supervisor.localize("common.update")}
</a> </ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -160,6 +160,27 @@ class HassioCoreInfo extends LitElement {
} }
} }
private async _coreUpdate(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@@ -218,9 +239,6 @@ class HassioCoreInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -9,7 +9,6 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
@@ -21,6 +20,7 @@ import {
configSyncOS, configSyncOS,
rebootHost, rebootHost,
shutdownHost, shutdownHost,
updateOS,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { import {
fetchNetworkInfo, fetchNetworkInfo,
@@ -105,15 +105,11 @@ class HassioHostInfo extends LitElement {
<span slot="description"> <span slot="description">
${this.supervisor.host.operating_system} ${this.supervisor.host.operating_system}
</span> </span>
${!atLeastVersion(this.hass.config.version, 2021, 12) && ${this.supervisor.os.update_available
this.supervisor.os.update_available
? html` ? html`
<a href="/hassio/update-available/os"> <ha-progress-button @click=${this._osUpdate}>
<mwc-button ${this.supervisor.localize("commmon.update")}
.label=${this.supervisor.localize("common.show")} </ha-progress-button>
>
</mwc-button>
</a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -185,11 +181,9 @@ class HassioHostInfo extends LitElement {
: ""} : ""}
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START">
<ha-icon-button <mwc-icon-button slot="trigger">
.label=${this.hass.localize("common.menu")} <ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
.path=${mdiDotsVertical} </mwc-icon-button>
slot="trigger"
></ha-icon-button>
<mwc-list-item <mwc-list-item
.action=${"hardware"} .action=${"hardware"}
@click=${this._handleMenuAction} @click=${this._handleMenuAction}
@@ -336,6 +330,50 @@ class HassioHostInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err: any) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> { private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, { showNetworkDialog(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
@@ -453,9 +491,6 @@ class HassioHostInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -17,6 +17,7 @@ import {
restartSupervisor, restartSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
@@ -30,9 +31,29 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = {}; const UNSUPPORTED_REASON_URL = {
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container",
content_trust: "/more-info/unsupported/content_trust",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os_agent: "/more-info/unsupported/os_agent",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
source_mods: "/more-info/unsupported/source_mods",
systemd: "/more-info/unsupported/systemd",
};
const UNHEALTHY_REASON_URL = { const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged", privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
}; };
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
@@ -76,15 +97,16 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description"> <span slot="description">
supervisor-${this.supervisor.supervisor.version_latest} supervisor-${this.supervisor.supervisor.version_latest}
</span> </span>
${!atLeastVersion(this.hass.config.version, 2021, 12) && ${this.supervisor.supervisor.update_available
this.supervisor.supervisor.update_available
? html` ? html`
<a href="/hassio/update-available/supervisor"> <ha-progress-button
<mwc-button .title=${this.supervisor.localize(
.label=${this.supervisor.localize("common.show")} "system.supervisor.update_supervisor"
> )}
</mwc-button> @click=${this._supervisorUpdate}
</a> >
${this.supervisor.localize("common.update")}
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -151,28 +173,24 @@ class HassioSupervisorInfo extends LitElement {
></ha-switch> ></ha-switch>
</ha-settings-row>` </ha-settings-row>`
: "" : ""
: html`<ha-alert alert-type="warning"> : html`<ha-alert
alert-type="warning"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unsupportedDialog}
>
${this.supervisor.localize( ${this.supervisor.localize(
"system.supervisor.unsupported_title" "system.supervisor.unsupported_title"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
</mwc-button>
</ha-alert>`} </ha-alert>`}
${!this.supervisor.supervisor.healthy ${!this.supervisor.supervisor.healthy
? html`<ha-alert alert-type="error"> ? html`<ha-alert
alert-type="error"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unhealthyDialog}
>
${this.supervisor.localize( ${this.supervisor.localize(
"system.supervisor.unhealthy_title" "system.supervisor.unhealthy_title"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
</mwc-button>
</ha-alert>` </ha-alert>`
: ""} : ""}
</div> </div>
@@ -339,6 +357,51 @@ class HassioSupervisorInfo extends LitElement {
} }
} }
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _diagnosticsInformationDialog(): Promise<void> { private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, { await showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
@@ -362,19 +425,20 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unsupported.map( ${this.supervisor.resolution.unsupported.map(
(reason) => html` (reason) => html`
<li> <li>
<a ${UNSUPPORTED_REASON_URL[reason]
href=${documentationUrl( ? html`<a
this.hass, href=${documentationUrl(
UNSUPPORTED_REASON_URL[reason] || this.hass,
`/more-info/unsupported/${reason}` UNSUPPORTED_REASON_URL[reason]
)} )}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.supervisor.localize( ${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}` `system.supervisor.unsupported_reason.${reason}`
) || reason} ) || reason}
</a> </a>`
: reason}
</li> </li>
` `
)} )}
@@ -392,19 +456,20 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unhealthy.map( ${this.supervisor.resolution.unhealthy.map(
(reason) => html` (reason) => html`
<li> <li>
<a ${UNHEALTHY_REASON_URL[reason]
href=${documentationUrl( ? html`<a
this.hass, href=${documentationUrl(
UNHEALTHY_REASON_URL[reason] || this.hass,
`/more-info/unhealthy/${reason}` UNHEALTHY_REASON_URL[reason]
)} )}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.supervisor.localize( ${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}` `system.supervisor.unhealthy_reason.${reason}`
) || reason} ) || reason}
</a> </a>`
: reason}
</li> </li>
` `
)} )}
@@ -470,12 +535,6 @@ class HassioSupervisorInfo extends LitElement {
white-space: normal; white-space: normal;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -28,7 +28,7 @@ class HassioSystem extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs(this.hass)} .tabs=${supervisorTabs}
main-page main-page
supervisor supervisor
> >

View File

@@ -1,401 +0,0 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
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-checkbox";
import "../../../src/components/ha-expansion-panel";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { 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 "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
hass: HomeAssistant,
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"
: documentationUrl(hass, "/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!: boolean;
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: updateType;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (
!this._updateType ||
(this._updateType === "addon" && !this._addonInfo)
) {
return html``;
}
const changelog = changelogUrl(this.hass, this._updateType, this._version);
return html`
<ha-card
.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._action === null
? html`
${this._changelogContent
? html`
<ha-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize("update_available.description", {
name: this._name,
version: this._version,
newest_version: this._version_latest,
})}
</p>
</div>
${["core", "addon"].includes(this._updateType)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info.state !== "running")}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</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;
if (updateType === "addon") {
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
}
}
get _shouldCreateBackup(): boolean {
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version
: this.supervisor[this._updateType]?.version || ""
: "";
}
get _version_latest(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version_latest
: this.supervisor[this._updateType]?.version_latest || ""
: "";
}
get _name(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.name
: SUPERVISOR_UPDATE_NAMES[this._updateType]
: "";
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addonSlug!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private async _update() {
this._error = undefined;
if (this._shouldCreateBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
} else if (this._updateType === "core") {
await updateCore(this.hass);
} 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._action = null;
return;
}
}
fireEvent(this, "update-complete");
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
ha-formfield {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-card": UpdateAvailableCard;
}
}

View File

@@ -1,59 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-subpage";
import { 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!: boolean;
@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() {
history.back();
}
static get styles(): CSSResultGroup {
return 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;
max-width: 600px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-dashboard": UpdateAvailableDashboard;
}
}

View File

@@ -1,30 +1,7 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { SupervisorArch } from "../../../src/data/supervisor/supervisor"; import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
export const addonArchIsSupported = memoizeOne( export const addonArchIsSupported = memoizeOne(
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) => (supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
addon_archs.some((arch) => supported_archs.includes(arch)) 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;
};

View File

@@ -22,23 +22,23 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.2", "@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.5", "@codemirror/commands": "^0.19.2",
"@codemirror/gutter": "^0.19.4", "@codemirror/gutter": "^0.19.1",
"@codemirror/highlight": "^0.19.6", "@codemirror/highlight": "^0.19.2",
"@codemirror/history": "^0.19.0", "@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1", "@codemirror/rectangular-selection": "^0.19.0",
"@codemirror/search": "^0.19.2", "@codemirror/search": "^0.19.0",
"@codemirror/state": "^0.19.4", "@codemirror/state": "^0.19.1",
"@codemirror/stream-parser": "^0.19.2", "@codemirror/stream-parser": "^0.19.1",
"@codemirror/text": "^0.19.5", "@codemirror/text": "^0.19.2",
"@codemirror/view": "^0.19.15", "@codemirror/view": "^0.19.4",
"@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-datetimeformat": "^4.2.4",
"@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-getcanonicallocales": "^1.7.3",
"@formatjs/intl-locale": "^2.4.40", "@formatjs/intl-locale": "^2.4.38",
"@formatjs/intl-numberformat": "^7.2.5", "@formatjs/intl-numberformat": "^7.2.4",
"@formatjs/intl-pluralrules": "^4.1.5", "@formatjs/intl-pluralrules": "^4.1.4",
"@formatjs/intl-relativetimeformat": "^9.3.2", "@formatjs/intl-relativetimeformat": "^9.3.1",
"@formatjs/intl-utils": "^3.8.4", "@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0", "@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0", "@fullcalendar/core": "5.9.0",
@@ -46,38 +46,45 @@
"@fullcalendar/interaction": "5.9.0", "@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0", "@fullcalendar/list": "5.9.0",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
"@material/chips": "14.0.0-canary.261f2db59.0", "@material/chips": "13.0.0-canary.65125b3a6.0",
"@material/data-table": "14.0.0-canary.261f2db59.0", "@material/data-table": "13.0.0-canary.65125b3a6.0",
"@material/mwc-button": "0.25.3", "@material/mwc-button": "0.25.1",
"@material/mwc-checkbox": "0.25.3", "@material/mwc-checkbox": "0.25.1",
"@material/mwc-circular-progress": "0.25.3", "@material/mwc-circular-progress": "0.25.1",
"@material/mwc-dialog": "0.25.3", "@material/mwc-dialog": "0.25.1",
"@material/mwc-fab": "0.25.3", "@material/mwc-fab": "0.25.1",
"@material/mwc-formfield": "0.25.3", "@material/mwc-formfield": "0.25.1",
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch", "@material/mwc-icon-button": "0.25.1",
"@material/mwc-linear-progress": "0.25.3", "@material/mwc-linear-progress": "0.25.1",
"@material/mwc-list": "0.25.3", "@material/mwc-list": "0.25.1",
"@material/mwc-menu": "0.25.3", "@material/mwc-menu": "0.25.1",
"@material/mwc-radio": "0.25.3", "@material/mwc-radio": "0.25.1",
"@material/mwc-ripple": "0.25.3", "@material/mwc-ripple": "0.25.1",
"@material/mwc-select": "0.25.3", "@material/mwc-switch": "0.25.1",
"@material/mwc-slider": "0.25.3", "@material/mwc-tab": "0.25.1",
"@material/mwc-switch": "0.25.3", "@material/mwc-tab-bar": "0.25.1",
"@material/mwc-tab": "0.25.3", "@material/top-app-bar": "13.0.0-canary.65125b3a6.0",
"@material/mwc-tab-bar": "0.25.3", "@mdi/js": "6.1.95",
"@material/mwc-textfield": "0.25.3", "@mdi/svg": "6.1.95",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1", "@polymer/iron-input": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1", "@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
"@polymer/paper-dialog": "^3.0.1",
"@polymer/paper-dialog-behavior": "^3.0.1",
"@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0", "@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1", "@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1", "@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1", "@polymer/paper-listbox": "^3.0.1",
"@polymer/paper-menu-button": "^3.1.0",
"@polymer/paper-progress": "^3.0.1",
"@polymer/paper-radio-button": "^3.0.1",
"@polymer/paper-radio-group": "^3.0.1",
"@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1", "@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1", "@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0", "@polymer/paper-tabs": "^3.1.0",
@@ -101,14 +108,14 @@
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.0.11", "hls.js": "^1.0.10",
"home-assistant-js-websocket": "^5.11.1", "home-assistant-js-websocket": "^5.11.1",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"lit": "^2.0.2", "lit": "^2.0.0",
"lit-vaadin-helpers": "^0.2.1", "lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2", "marked": "^3.0.2",
"memoize-one": "^5.2.1", "memoize-one": "^5.2.1",
@@ -180,7 +187,7 @@
"eslint-import-resolver-webpack": "^0.13.1", "eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1", "eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-lit": "^1.6.1", "eslint-plugin-lit": "^1.5.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^1.1.5", "eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2", "eslint-plugin-wc": "^1.3.2",
@@ -230,10 +237,10 @@
"resolutions": { "resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", "@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10", "@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.0.2", "lit": "^2.0.0",
"lit-html": "2.0.1", "lit-html": "2.0.0",
"lit-element": "3.0.1", "lit-element": "3.0.0",
"@lit/reactive-element": "1.0.1" "@lit/reactive-element": "1.0.0"
}, },
"main": "src/home-assistant.js", "main": "src/home-assistant.js",
"husky": { "husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20211123.0", version="20210930.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend", url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { genClientId } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -8,12 +7,9 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import "../components/ha-alert";
import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
import "../components/ha-form/ha-form"; import "../components/ha-form/ha-form";
import "../components/ha-formfield";
import "../components/ha-markdown"; import "../components/ha-markdown";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";
import { import {
@@ -21,7 +17,6 @@ import {
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@@ -36,44 +31,12 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _state: State = "loading"; @state() private _state: State = "loading";
@state() private _stepData?: Record<string, any>; @state() private _stepData: any = {};
@state() private _step?: DataEntryFlowStep; @state() private _step?: DataEntryFlowStep;
@state() private _errorMessage?: string; @state() private _errorMessage?: string;
@state() private _submitting = false;
@state() private _storeToken = false;
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("_step")) {
return;
}
if (!this._step) {
this._stepData = undefined;
return;
}
const oldStep = changedProps.get("_step") as HaAuthFlow["_step"];
if (
!oldStep ||
this._step.flow_id !== oldStep.flow_id ||
(this._step.type === "form" &&
oldStep.type === "form" &&
this._step.step_id !== oldStep.step_id)
) {
this._stepData =
this._step.type === "form"
? computeInitialHaFormData(this._step.data_schema)
: undefined;
}
}
protected render() { protected render() {
return html` return html`
<form>${this._renderForm()}</form> <form>${this._renderForm()}</form>
@@ -113,24 +76,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (changedProps.has("authProvider")) { if (changedProps.has("authProvider")) {
this._providerChanged(this.authProvider); this._providerChanged(this.authProvider);
} }
if (!changedProps.has("_step") || this._step?.type !== "form") {
return;
}
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
}
}, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
} }
private _renderForm(): TemplateResult { private _renderForm(): TemplateResult {
@@ -142,33 +87,27 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return html` return html`
${this._renderStep(this._step)} ${this._renderStep(this._step)}
<div class="action"> <div class="action">
<mwc-button <mwc-button raised @click=${this._handleSubmit}
raised >${this._step.type === "form"
@click=${this._handleSubmit}
.disabled=${this._submitting}
>
${this._step.type === "form"
? this.localize("ui.panel.page-authorize.form.next") ? this.localize("ui.panel.page-authorize.form.next")
: this.localize("ui.panel.page-authorize.form.start_over")} : this.localize(
</mwc-button> "ui.panel.page-authorize.form.start_over"
)}</mwc-button
>
</div> </div>
`; `;
case "error": case "error":
return html` return html`
<ha-alert alert-type="error"> <div class="error">
${this.localize( ${this.localize(
"ui.panel.page-authorize.form.error", "ui.panel.page-authorize.form.error",
"error", "error",
this._errorMessage this._errorMessage
)} )}
</ha-alert> </div>
`; `;
case "loading": case "loading":
return html` return html` ${this.localize("ui.panel.page-authorize.form.working")} `;
<ha-alert alert-type="info">
${this.localize("ui.panel.page-authorize.form.working")}
</ha-alert>
`;
default: default:
return html``; return html``;
} }
@@ -201,35 +140,16 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.data=${this._stepData} .data=${this._stepData}
.schema=${step.data_schema} .schema=${step.data_schema}
.error=${step.errors} .error=${step.errors}
.disabled=${this._submitting}
.computeLabel=${this._computeLabelCallback(step)} .computeLabel=${this._computeLabelCallback(step)}
.computeError=${this._computeErrorCallback(step)} .computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged} @value-changed=${this._stepDataChanged}
></ha-form> ></ha-form>
${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id)
? html`
<ha-formfield
class="store-token"
.label=${this.localize("ui.panel.page-authorize.store_token")}
>
<ha-checkbox
.checked=${this._storeToken}
@change=${this._storeTokenChanged}
></ha-checkbox>
</ha-formfield>
`
: ""}
`; `;
default: default:
return html``; return html``;
} }
} }
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
}
private async _providerChanged(newProvider?: AuthProvider) { private async _providerChanged(newProvider?: AuthProvider) {
if (this._step && this._step.type === "form") { if (this._step && this._step.type === "form") {
fetch(`/auth/login_flow/${this._step.flow_id}`, { fetch(`/auth/login_flow/${this._step.flow_id}`, {
@@ -269,8 +189,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return; return;
} }
this._step = data; await this._updateStep(data);
this._state = "step";
} else { } else {
this._state = "error"; this._state = "error";
this._errorMessage = data.message; this._errorMessage = data.message;
@@ -297,13 +216,43 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (this.oauth2State) { if (this.oauth2State) {
url += `&state=${encodeURIComponent(this.oauth2State)}`; url += `&state=${encodeURIComponent(this.oauth2State)}`;
} }
if (this._storeToken) {
url += `&storeToken=true`;
}
document.location.assign(url); document.location.assign(url);
} }
private async _updateStep(step: DataEntryFlowStep) {
let stepData: any = null;
if (
this._step &&
(step.flow_id !== this._step.flow_id ||
(step.type === "form" &&
this._step.type === "form" &&
step.step_id !== this._step.step_id))
) {
stepData = {};
}
this._step = step;
this._state = "step";
if (stepData != null) {
this._stepData = stepData;
}
await this.updateComplete;
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
}
}, 100);
setTimeout(() => {
this.renderRoot.querySelector(
"ha-password-manager-polyfill"
)!.boundingRect = this.getBoundingClientRect();
}, 500);
}
private _stepDataChanged(ev: CustomEvent) { private _stepDataChanged(ev: CustomEvent) {
this._stepData = ev.detail.value; this._stepData = ev.detail.value;
} }
@@ -348,7 +297,9 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._providerChanged(this.authProvider); this._providerChanged(this.authProvider);
return; return;
} }
this._submitting = true; this._state = "loading";
// To avoid a jumping UI.
this.style.setProperty("min-height", `${this.offsetHeight}px`);
const postData = { ...this._stepData, client_id: this.clientId }; const postData = { ...this._stepData, client_id: this.clientId };
@@ -365,28 +316,29 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._redirect(newStep.result); this._redirect(newStep.result);
return; return;
} }
this._step = newStep; await this._updateStep(newStep);
this._state = "step";
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Error submitting step", err); console.error("Error submitting step", err);
this._state = "error"; this._state = "error";
this._errorMessage = this._unknownError(); this._errorMessage = this._unknownError();
} finally { } finally {
this._submitting = false; this.style.setProperty("min-height", "");
} }
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
/* So we can set min-height to avoid jumping during loading */
display: block;
}
.action { .action {
margin: 24px 0 8px; margin: 24px 0 8px;
text-align: center; text-align: center;
} }
/* Align with the rest of the form. */ .error {
.store-token { color: red;
margin-top: 10px;
margin-left: -16px;
} }
`; `;
} }

View File

@@ -174,10 +174,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
display: block; display: block;
margin-top: 48px; margin-top: 48px;
} }
ha-auth-flow {
display: block;
margin-top: 24px;
}
`; `;
} }
} }

View File

@@ -2,8 +2,8 @@
import { html, LitElement, TemplateResult } from "lit"; import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { HaFormSchema } from "../components/ha-form/types"; import { HaFormSchema } from "../components/ha-form/ha-form";
import type { DataEntryFlowStep } from "../data/data_entry_flow"; import { DataEntryFlowStep } from "../data/data_entry_flow";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -21,11 +21,7 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p> <p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
${this.authProviders.map( ${this.authProviders.map(
(provider) => html` (provider) => html`
<paper-item <paper-item .auth_provider=${provider} @click=${this._handlePick}>
role="button"
.auth_provider=${provider}
@click=${this._handlePick}
>
<paper-item-body>${provider.name}</paper-item-body> <paper-item-body>${provider.name}</paper-item-body>
<ha-icon-next></ha-icon-next> <ha-icon-next></ha-icon-next>
</paper-item> </paper-item>

View File

@@ -3,5 +3,5 @@ import { CAST_DEV_APP_ID } from "./dev_const";
// Guard dev mode with `__dev__` so it can only ever be enabled in dev mode. // Guard dev mode with `__dev__` so it can only ever be enabled in dev mode.
export const CAST_DEV = __DEV__ && true; export const CAST_DEV = __DEV__ && true;
export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "A078F6B0"; export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA";
export const CAST_NS = "urn:x-cast:com.nabucasa.hast"; export const CAST_NS = "urn:x-cast:com.nabucasa.hast";

View File

@@ -11,20 +11,4 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
urlPath?: string | null; urlPath?: string | null;
} }
export interface ReceiverErrorMessage extends BaseCastMessage {
type: "receiver_error";
error_code: ReceiverErrorCode;
error_message: string;
}
export const enum ReceiverErrorCode {
CONNECTION_FAILED = 1,
AUTHENTICATION_FAILED = 2,
CONNECTION_LOST = 3,
HASS_URL_MISSING = 4,
NO_HTTPS = 5,
NOT_CONNECTED = 21,
FETCH_CONFIG_FAILED = 22,
}
export type SenderMessage = ReceiverStatusMessage; export type SenderMessage = ReceiverStatusMessage;

View File

@@ -1,5 +1,4 @@
import { AuthData } from "home-assistant-js-websocket"; import { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
const storage = window.localStorage || {}; const storage = window.localStorage || {};
@@ -31,11 +30,6 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) { export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens; tokenCache.tokens = tokens;
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
tokenCache.writeEnabled = true;
}
if (tokenCache.writeEnabled) { if (tokenCache.writeEnabled) {
try { try {
storage.hassTokens = JSON.stringify(tokens); storage.hassTokens = JSON.stringify(tokens);
@@ -51,6 +45,7 @@ export function enableWrite() {
saveTokens(tokenCache.tokens); saveTokens(tokenCache.tokens);
} }
} }
export function loadTokens() { export function loadTokens() {
if (tokenCache.tokens === undefined) { if (tokenCache.tokens === undefined) {
try { try {

View File

@@ -7,13 +7,7 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page); !hideAdvancedPage(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) => const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
page.component !page.component || isComponentLoaded(hass, page.component);
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
const isCore = (page: PageNavigation) => page.core; const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly; const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced; const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

View File

@@ -4,10 +4,6 @@ export const atLeastVersion = (
minor: number, minor: number,
patch?: number patch?: number
): boolean => { ): boolean => {
if (__DEMO__) {
return true;
}
const [haMajor, haMinor, haPatch] = version.split(".", 3); const [haMajor, haMinor, haPatch] = version.split(".", 3);
return ( return (

View File

@@ -1,150 +1,91 @@
/** Constants to be used in the frontend. */ /** Constants to be used in the frontend. */
import {
mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAngleAcute,
mdiAppleSafari,
mdiBell,
mdiBookmark,
mdiBrightness5,
mdiBullhorn,
mdiCalendar,
mdiCalendarClock,
mdiCash,
mdiClock,
mdiCloudUpload,
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiCurrentAc,
mdiEye,
mdiFan,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiGasCylinder,
mdiGauge,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAssistant,
mdiHomeAutomation,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
mdiMailbox,
mdiMapMarkerRadius,
mdiMolecule,
mdiMoleculeCo,
mdiMoleculeCo2,
mdiPalette,
mdiRayVertex,
mdiRemote,
mdiRobot,
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiTextToSpeech,
mdiThermometer,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitchOutline,
mdiVideo,
mdiWaterPercent,
mdiWeatherCloudy,
mdiWhiteBalanceSunny,
mdiWifi,
} from "@mdi/js";
// Constants should be alphabetically sorted by name. // Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter. // Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for. // Each constant should have a description what it is supposed to be used for.
/** Icon to use when no icon specified for domain. */ /** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = mdiBookmark; export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
/** Icons for each domain */ /** Icons for each domain */
export const FIXED_DOMAIN_ICONS = { export const FIXED_DOMAIN_ICONS = {
alert: mdiAlert, alert: "hass:alert",
air_quality: mdiAirFilter, alexa: "hass:amazon-alexa",
automation: mdiRobot, air_quality: "hass:air-filter",
calendar: mdiCalendar, automation: "hass:robot",
camera: mdiVideo, calendar: "hass:calendar",
climate: mdiThermostat, camera: "hass:video",
configurator: mdiCog, climate: "hass:thermostat",
conversation: mdiTextToSpeech, configurator: "hass:cog",
counter: mdiCounter, conversation: "hass:text-to-speech",
fan: mdiFan, counter: "hass:counter",
google_assistant: mdiGoogleAssistant, device_tracker: "hass:account",
group: mdiGoogleCirclesCommunities, fan: "hass:fan",
homeassistant: mdiHomeAssistant, google_assistant: "hass:google-assistant",
homekit: mdiHomeAutomation, group: "hass:google-circles-communities",
image_processing: mdiImageFilterFrames, homeassistant: "hass:home-assistant",
input_boolean: mdiToggleSwitchOutline, homekit: "hass:home-automation",
input_datetime: mdiCalendarClock, image_processing: "hass:image-filter-frames",
input_number: mdiRayVertex, input_boolean: "hass:toggle-switch-outline",
input_select: mdiFormatListBulleted, input_datetime: "hass:calendar-clock",
input_text: mdiFormTextbox, input_number: "hass:ray-vertex",
light: mdiLightbulb, input_select: "hass:format-list-bulleted",
mailbox: mdiMailbox, input_text: "hass:form-textbox",
notify: mdiCommentAlert, light: "hass:lightbulb",
number: mdiRayVertex, mailbox: "hass:mailbox",
persistent_notification: mdiBell, notify: "hass:comment-alert",
person: mdiAccount, number: "hass:ray-vertex",
plant: mdiFlower, persistent_notification: "hass:bell",
proximity: mdiAppleSafari, person: "hass:account",
remote: mdiRemote, plant: "hass:flower",
scene: mdiPalette, proximity: "hass:apple-safari",
script: mdiScriptText, remote: "hass:remote",
select: mdiFormatListBulleted, scene: "hass:palette",
sensor: mdiEye, script: "hass:script-text",
siren: mdiBullhorn, select: "hass:format-list-bulleted",
simple_alarm: mdiBell, sensor: "hass:eye",
sun: mdiWhiteBalanceSunny, simple_alarm: "hass:bell",
timer: mdiTimerOutline, sun: "hass:white-balance-sunny",
updater: mdiCloudUpload, switch: "hass:flash",
vacuum: mdiRobotVacuum, timer: "hass:timer-outline",
water_heater: mdiThermometer, updater: "hass:cloud-upload",
weather: mdiWeatherCloudy, vacuum: "hass:robot-vacuum",
zone: mdiMapMarkerRadius, water_heater: "hass:thermometer",
weather: "hass:weather-cloudy",
zone: "hass:map-marker-radius",
}; };
export const FIXED_DEVICE_CLASS_ICONS = { export const FIXED_DEVICE_CLASS_ICONS = {
aqi: mdiAirFilter, aqi: "hass:air-filter",
// battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon current: "hass:current-ac",
carbon_dioxide: mdiMoleculeCo2, carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: mdiMoleculeCo, carbon_monoxide: "mdi:molecule-co",
current: mdiCurrentAc, date: "hass:calendar",
date: mdiCalendar, energy: "hass:lightning-bolt",
energy: mdiLightningBolt, gas: "hass:gas-cylinder",
frequency: mdiSineWave, humidity: "hass:water-percent",
gas: mdiGasCylinder, illuminance: "hass:brightness-5",
humidity: mdiWaterPercent, nitrogen_dioxide: "mdi:molecule",
illuminance: mdiBrightness5, nitrogen_monoxide: "mdi:molecule",
monetary: mdiCash, nitrous_oxide: "mdi:molecule",
nitrogen_dioxide: mdiMolecule, ozone: "mdi:molecule",
nitrogen_monoxide: mdiMolecule, temperature: "hass:thermometer",
nitrous_oxide: mdiMolecule, monetary: "mdi:cash",
ozone: mdiMolecule, pm25: "mdi:molecule",
pm1: mdiMolecule, pm1: "mdi:molecule",
pm10: mdiMolecule, pm10: "mdi:molecule",
pm25: mdiMolecule, pressure: "hass:gauge",
power: mdiFlash, power: "hass:flash",
power_factor: mdiAngleAcute, power_factor: "hass:angle-acute",
pressure: mdiGauge, signal_strength: "hass:wifi",
signal_strength: mdiWifi, sulphur_dioxide: "mdi:molecule",
sulphur_dioxide: mdiMolecule, timestamp: "hass:clock",
temperature: mdiThermometer, volatile_organic_compounds: "mdi:molecule",
timestamp: mdiClock, voltage: "hass:sine-wave",
volatile_organic_compounds: mdiMolecule,
voltage: mdiSineWave,
}; };
/** Domains that have a state card. */ /** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [ export const DOMAINS_WITH_CARD = [
"button",
"climate", "climate",
"cover", "cover",
"configurator", "configurator",

View File

@@ -36,62 +36,55 @@ export const applyThemesOnElement = (
let cacheKey = selectedTheme; let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {}; let themeRules: Partial<ThemeVars> = {};
// If there is no explicitly desired dark mode provided, we automatically if (themeSettings) {
// use the active one from hass.themes. if (themeSettings.dark) {
if (!themeSettings || themeSettings?.dark === undefined) { cacheKey = `${cacheKey}__dark`;
themeSettings = { themeRules = { ...darkStyles };
...themeSettings,
dark: themes.darkMode,
};
}
if (themeSettings.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
if (themeSettings.dark && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
8
);
} }
if (primaryColor) { if (selectedTheme === "default") {
cacheKey = `${cacheKey}__primary_${primaryColor}`; // Determine the primary and accent colors from the current settings.
const rgbPrimaryColor = hex2rgb(primaryColor); // Fallbacks are implicitly the HA default blue and orange or the
const labPrimaryColor = rgb2lab(rgbPrimaryColor); // derived "darkStyles" values, depending on the light vs dark mode.
themeRules["primary-color"] = primaryColor; const primaryColor = themeSettings.primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor)); const accentColor = themeSettings.accentColor;
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed if (themeSettings.dark && primaryColor) {
if (element._themes?.cacheKey === cacheKey) { themeRules["app-header-background-color"] = hexBlend(
return; primaryColor,
"#121212",
8
);
}
if (primaryColor) {
cacheKey = `${cacheKey}__primary_${primaryColor}`;
const rgbPrimaryColor = hex2rgb(primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = primaryColor;
const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (accentColor) {
cacheKey = `${cacheKey}__accent_${accentColor}`;
themeRules["accent-color"] = accentColor;
const rgbAccentColor = hex2rgb(accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
} }
} }
@@ -122,7 +115,7 @@ export const applyThemesOnElement = (
} }
const newTheme = const newTheme =
Object.keys(themeRules).length && cacheKey themeRules && cacheKey
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules) ? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
: undefined; : undefined;

View File

@@ -1,3 +1,2 @@
/** An empty image which can be set as src of an img element. */ /** An empty image which can be set as src of an img element. */
export const emptyImageBase64 = export default "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";

View File

@@ -1,36 +1,24 @@
/** Return an icon representing a alarm panel state. */ /** Return an icon representing a alarm panel state. */
import {
mdiShieldLock,
mdiShieldAirplane,
mdiShieldHome,
mdiShieldMoon,
mdiSecurity,
mdiShieldOutline,
mdiBellRing,
mdiShieldOff,
mdiShield,
} from "@mdi/js";
export const alarmPanelIcon = (state?: string) => { export const alarmPanelIcon = (state?: string) => {
switch (state) { switch (state) {
case "armed_away": case "armed_away":
return mdiShieldLock; return "hass:shield-lock";
case "armed_vacation": case "armed_vacation":
return mdiShieldAirplane; return "hass:shield-airplane";
case "armed_home": case "armed_home":
return mdiShieldHome; return "hass:shield-home";
case "armed_night": case "armed_night":
return mdiShieldMoon; return "hass:shield-moon";
case "armed_custom_bypass": case "armed_custom_bypass":
return mdiSecurity; return "hass:security";
case "pending": case "pending":
return mdiShieldOutline; return "hass:shield-outline";
case "triggered": case "triggered":
return mdiBellRing; return "hass:bell-ring";
case "disarmed": case "disarmed":
return mdiShieldOff; return "hass:shield-off";
default: default:
return mdiShield; return "hass:shield";
} }
}; };

View File

@@ -1,92 +1,35 @@
/** Return an icon representing a battery state. */ /** Return an icon representing a battery state. */
import {
mdiBattery,
mdiBattery10,
mdiBattery20,
mdiBattery30,
mdiBattery40,
mdiBattery50,
mdiBattery60,
mdiBattery70,
mdiBattery80,
mdiBattery90,
mdiBatteryAlert,
mdiBatteryAlertVariantOutline,
mdiBatteryCharging,
mdiBatteryCharging10,
mdiBatteryCharging20,
mdiBatteryCharging30,
mdiBatteryCharging40,
mdiBatteryCharging50,
mdiBatteryCharging60,
mdiBatteryCharging70,
mdiBatteryCharging80,
mdiBatteryCharging90,
mdiBatteryChargingOutline,
mdiBatteryUnknown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
const BATTERY_ICONS = { export const batteryIcon = (
10: mdiBattery10,
20: mdiBattery20,
30: mdiBattery30,
40: mdiBattery40,
50: mdiBattery50,
60: mdiBattery60,
70: mdiBattery70,
80: mdiBattery80,
90: mdiBattery90,
100: mdiBattery,
};
const BATTERY_CHARGING_ICONS = {
10: mdiBatteryCharging10,
20: mdiBatteryCharging20,
30: mdiBatteryCharging30,
40: mdiBatteryCharging40,
50: mdiBatteryCharging50,
60: mdiBatteryCharging60,
70: mdiBatteryCharging70,
80: mdiBatteryCharging80,
90: mdiBatteryCharging90,
100: mdiBatteryCharging,
};
export const batteryStateIcon = (
batteryState: HassEntity, batteryState: HassEntity,
batteryChargingState?: HassEntity batteryChargingState?: HassEntity
) => { ) => {
const battery = batteryState.state; const battery = Number(batteryState.state);
const batteryCharging = const battery_charging =
batteryChargingState && batteryChargingState.state === "on"; batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
return batteryIcon(battery, batteryCharging); if (isNaN(battery)) {
}; if (batteryState.state === "off") {
icon += "-full";
export const batteryIcon = ( } else if (batteryState.state === "on") {
batteryState: number | string, icon += "-alert";
batteryCharging?: boolean } else {
) => { icon += "-unknown";
const batteryValue = Number(batteryState);
if (isNaN(batteryValue)) {
if (batteryState === "off") {
return mdiBattery;
} }
if (batteryState === "on") { return icon;
return mdiBatteryAlert;
}
return mdiBatteryUnknown;
} }
const batteryRound = Math.round(batteryValue / 10) * 10; const batteryRound = Math.round(battery / 10) * 10;
if (batteryCharging && batteryValue >= 10) { if (battery_charging && battery > 10) {
return BATTERY_CHARGING_ICONS[batteryRound]; icon += `-charging-${batteryRound}`;
} else if (battery_charging) {
icon += "-outline";
} else if (battery <= 5) {
icon += "-alert";
} else if (battery > 5 && battery < 95) {
icon += `-${batteryRound}`;
} }
if (batteryCharging) { return icon;
return mdiBatteryChargingOutline;
}
if (batteryValue <= 5) {
return mdiBatteryAlertVariantOutline;
}
return BATTERY_ICONS[batteryRound];
}; };

View File

@@ -1,46 +1,3 @@
import {
mdiAlertCircle,
mdiBattery,
mdiBatteryCharging,
mdiBatteryOutline,
mdiBrightness5,
mdiBrightness7,
mdiCheckboxMarkedCircle,
mdiCheckNetworkOutline,
mdiCloseNetworkOutline,
mdiCheckCircle,
mdiCropPortrait,
mdiDoorClosed,
mdiDoorOpen,
mdiFire,
mdiGarage,
mdiGarageOpen,
mdiHome,
mdiHomeOutline,
mdiLock,
mdiLockOpen,
mdiMusicNote,
mdiMusicNoteOff,
mdiPackage,
mdiPackageUp,
mdiPlay,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiRun,
mdiSmoke,
mdiSnowflake,
mdiSquare,
mdiSquareOutline,
mdiStop,
mdiThermometer,
mdiVibrate,
mdiWalk,
mdiWater,
mdiWaterOff,
mdiWindowClosed,
mdiWindowOpen,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
@@ -49,55 +6,52 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
const is_off = state === "off"; const is_off = state === "off";
switch (stateObj?.attributes.device_class) { switch (stateObj?.attributes.device_class) {
case "battery": case "battery":
return is_off ? mdiBattery : mdiBatteryOutline; return is_off ? "hass:battery" : "hass:battery-outline";
case "battery_charging": case "battery_charging":
return is_off ? mdiBattery : mdiBatteryCharging; return is_off ? "hass:battery" : "hass:battery-charging";
case "cold": case "cold":
return is_off ? mdiThermometer : mdiSnowflake; return is_off ? "hass:thermometer" : "hass:snowflake";
case "connectivity": case "connectivity":
return is_off ? mdiCloseNetworkOutline : mdiCheckNetworkOutline; return is_off ? "hass:server-network-off" : "hass:server-network";
case "door": case "door":
return is_off ? mdiDoorClosed : mdiDoorOpen; return is_off ? "hass:door-closed" : "hass:door-open";
case "garage_door": case "garage_door":
return is_off ? mdiGarage : mdiGarageOpen; return is_off ? "hass:garage" : "hass:garage-open";
case "power": case "power":
return is_off ? mdiPowerPlugOff : mdiPowerPlug; return is_off ? "hass:power-plug-off" : "hass:power-plug";
case "gas": case "gas":
case "problem": case "problem":
case "safety": case "safety":
case "tamper": return is_off ? "hass:check-circle" : "hass:alert-circle";
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke": case "smoke":
return is_off ? mdiCheckCircle : mdiSmoke; return is_off ? "hass:check-circle" : "hass:smoke";
case "heat": case "heat":
return is_off ? mdiThermometer : mdiFire; return is_off ? "hass:thermometer" : "hass:fire";
case "light": case "light":
return is_off ? mdiBrightness5 : mdiBrightness7; return is_off ? "hass:brightness-5" : "hass:brightness-7";
case "lock": case "lock":
return is_off ? mdiLock : mdiLockOpen; return is_off ? "hass:lock" : "hass:lock-open";
case "moisture": case "moisture":
return is_off ? mdiWaterOff : mdiWater; return is_off ? "hass:water-off" : "hass:water";
case "motion": case "motion":
return is_off ? mdiWalk : mdiRun; return is_off ? "hass:walk" : "hass:run";
case "occupancy": case "occupancy":
return is_off ? mdiHomeOutline : mdiHome; return is_off ? "hass:home-outline" : "hass:home";
case "opening": case "opening":
return is_off ? mdiSquare : mdiSquareOutline; return is_off ? "hass:square" : "hass:square-outline";
case "plug": case "plug":
return is_off ? mdiPowerPlugOff : mdiPowerPlug; return is_off ? "hass:power-plug-off" : "hass:power-plug";
case "presence": case "presence":
return is_off ? mdiHomeOutline : mdiHome; return is_off ? "hass:home-outline" : "hass:home";
case "running":
return is_off ? mdiStop : mdiPlay;
case "sound": case "sound":
return is_off ? mdiMusicNoteOff : mdiMusicNote; return is_off ? "hass:music-note-off" : "hass:music-note";
case "update": case "update":
return is_off ? mdiPackage : mdiPackageUp; return is_off ? "mdi:package" : "mdi:package-up";
case "vibration": case "vibration":
return is_off ? mdiCropPortrait : mdiVibrate; return is_off ? "hass:crop-portrait" : "hass:vibrate";
case "window": case "window":
return is_off ? mdiWindowClosed : mdiWindowOpen; return is_off ? "hass:window-closed" : "hass:window-open";
default: default:
return is_off ? mdiRadioboxBlank : mdiCheckboxMarkedCircle; return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
} }
}; };

View File

@@ -4,7 +4,7 @@ import { FrontendLocaleData } from "../../data/translation";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { formatNumber, isNumericState } from "../number/format_number"; import { formatNumber } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
@@ -20,8 +20,7 @@ export const computeStateDisplay = (
return localize(`state.default.${compareState}`); return localize(`state.default.${compareState}`);
} }
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` if (stateObj.attributes.unit_of_measurement) {
if (isNumericState(stateObj)) {
if (stateObj.attributes.device_class === "monetary") { if (stateObj.attributes.device_class === "monetary") {
try { try {
return formatNumber(compareState, locale, { return formatNumber(compareState, locale, {
@@ -32,17 +31,15 @@ export const computeStateDisplay = (
// fallback to default // fallback to default
} }
} }
return `${formatNumber(compareState, locale)}${ return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
? " " + stateObj.attributes.unit_of_measurement
: ""
}`; }`;
} }
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") { if (domain === "input_datetime") {
if (state !== undefined) { if (state) {
// If trying to display an explicit state, need to parse the explict state to `Date` then format. // If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`. // Attributes aren't available, we have to use `state`.
try { try {
@@ -66,7 +63,7 @@ export const computeStateDisplay = (
} }
} }
return state; return state;
} catch (_e) { } catch {
// Formatting methods may throw error if date parsing doesn't go well, // Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case. // just return the state string in that case.
return state; return state;
@@ -74,17 +71,7 @@ export const computeStateDisplay = (
} else { } else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format. // If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date; let date: Date;
if (stateObj.attributes.has_date && stateObj.attributes.has_time) { if (!stateObj.attributes.has_time) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
}
if (stateObj.attributes.has_date) {
date = new Date( date = new Date(
stateObj.attributes.year, stateObj.attributes.year,
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
@@ -92,12 +79,20 @@ export const computeStateDisplay = (
); );
return formatDate(date, locale); return formatDate(date, locale);
} }
if (stateObj.attributes.has_time) { if (!stateObj.attributes.has_date) {
date = new Date(); date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute); date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale); return formatTime(date, locale);
} }
return stateObj.state;
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
stateObj.attributes.day,
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, locale);
} }
} }
@@ -116,14 +111,6 @@ export const computeStateDisplay = (
return formatNumber(compareState, locale); return formatNumber(compareState, locale);
} }
// state of button is a timestamp
if (
domain === "button" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);
}
return ( return (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@@ -1,30 +1,4 @@
/** Return an icon representing a cover state. */ /** Return an icon representing a cover state. */
import {
mdiArrowUpBox,
mdiArrowDownBox,
mdiGarage,
mdiGarageOpen,
mdiGateArrowRight,
mdiGate,
mdiGateOpen,
mdiDoorOpen,
mdiDoorClosed,
mdiCircle,
mdiWindowShutter,
mdiWindowShutterOpen,
mdiBlinds,
mdiBlindsOpen,
mdiWindowClosed,
mdiWindowOpen,
mdiArrowExpandHorizontal,
mdiArrowUp,
mdiArrowCollapseHorizontal,
mdiArrowDown,
mdiCircleSlice8,
mdiArrowSplitVertical,
mdiCurtains,
mdiCurtainsClosed,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
export const coverIcon = (state?: string, stateObj?: HassEntity): string => { export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
@@ -34,84 +8,74 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
case "garage": case "garage":
switch (state) { switch (state) {
case "opening": case "opening":
return mdiArrowUpBox; return "hass:arrow-up-box";
case "closing": case "closing":
return mdiArrowDownBox; return "hass:arrow-down-box";
case "closed": case "closed":
return mdiGarage; return "hass:garage";
default: default:
return mdiGarageOpen; return "hass:garage-open";
} }
case "gate": case "gate":
switch (state) { switch (state) {
case "opening": case "opening":
case "closing": case "closing":
return mdiGateArrowRight; return "hass:gate-arrow-right";
case "closed": case "closed":
return mdiGate; return "hass:gate";
default: default:
return mdiGateOpen; return "hass:gate-open";
} }
case "door": case "door":
return open ? mdiDoorOpen : mdiDoorClosed; return open ? "hass:door-open" : "hass:door-closed";
case "damper": case "damper":
return open ? mdiCircle : mdiCircleSlice8; return open ? "hass:circle" : "hass:circle-slice-8";
case "shutter": case "shutter":
switch (state) { switch (state) {
case "opening": case "opening":
return mdiArrowUpBox; return "hass:arrow-up-box";
case "closing": case "closing":
return mdiArrowDownBox; return "hass:arrow-down-box";
case "closed": case "closed":
return mdiWindowShutter; return "hass:window-shutter";
default: default:
return mdiWindowShutterOpen; return "hass:window-shutter-open";
}
case "curtain":
switch (state) {
case "opening":
return mdiArrowSplitVertical;
case "closing":
return mdiArrowCollapseHorizontal;
case "closed":
return mdiCurtainsClosed;
default:
return mdiCurtains;
} }
case "blind": case "blind":
case "curtain":
case "shade": case "shade":
switch (state) { switch (state) {
case "opening": case "opening":
return mdiArrowUpBox; return "hass:arrow-up-box";
case "closing": case "closing":
return mdiArrowDownBox; return "hass:arrow-down-box";
case "closed": case "closed":
return mdiBlinds; return "hass:blinds";
default: default:
return mdiBlindsOpen; return "hass:blinds-open";
} }
case "window": case "window":
switch (state) { switch (state) {
case "opening": case "opening":
return mdiArrowUpBox; return "hass:arrow-up-box";
case "closing": case "closing":
return mdiArrowDownBox; return "hass:arrow-down-box";
case "closed": case "closed":
return mdiWindowClosed; return "hass:window-closed";
default: default:
return mdiWindowOpen; return "hass:window-open";
} }
} }
switch (state) { switch (state) {
case "opening": case "opening":
return mdiArrowUpBox; return "hass:arrow-up-box";
case "closing": case "closing":
return mdiArrowDownBox; return "hass:arrow-down-box";
case "closed": case "closed":
return mdiWindowClosed; return "hass:window-closed";
default: default:
return mdiWindowOpen; return "hass:window-open";
} }
}; };
@@ -120,9 +84,9 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
return mdiArrowExpandHorizontal; return "hass:arrow-expand-horizontal";
default: default:
return mdiArrowUp; return "hass:arrow-up";
} }
}; };
@@ -131,8 +95,8 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning": case "awning":
case "door": case "door":
case "gate": case "gate":
return mdiArrowCollapseHorizontal; return "hass:arrow-collapse-horizontal";
default: default:
return mdiArrowDown; return "hass:arrow-down";
} }
}; };

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