Compare commits

..

15 Commits

Author SHA1 Message Date
Paul Bottein
67a5152c36 Improve download backup (#22905) 2024-11-20 12:34:30 +01:00
Paul Bottein
918fca4d0a Add new backup dialog to choose between automatic and manual (#22895)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-11-20 12:32:25 +01:00
Paul Bottein
258a19028b Add backup details page (#22884) 2024-11-20 09:49:08 +01:00
Paul Bottein
7b4536564e Merge branch 'dev' into allthebackupchanges 2024-11-20 09:30:54 +01:00
Paul Bottein
d9cd428bf4 Create generate backup dialog (#22866)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-11-19 12:33:45 +01:00
Paul Bottein
be6ecefb9e Add delete backup action to datatable (#22867) 2024-11-19 11:53:15 +01:00
Paul Bottein
99bde50c01 Rename backup slug to backup id (#22876) 2024-11-19 09:45:48 +01:00
Paul Bottein
a2471f82a3 Merge branch 'dev' into allthebackupchanges 2024-11-19 09:34:15 +01:00
Paul Bottein
c90e820c7f Fix backup agents 2024-11-18 15:37:01 +01:00
Paul Bottein
0c0b657c79 Merge branch 'dev' into allthebackupchanges 2024-11-18 11:38:32 +01:00
Paul Bottein
8941837697 Improve backup screen and navigation (#22827)
* Add location page

* Start dashboard

* Move list to dashboard

* Add mocked config page

* Fix hardcoded boolean

* Add summary card

* Use new format for BackupAgent

* Use new API

* Rename to ha-backup-summary-card

* Use new api
2024-11-18 11:37:48 +01:00
Paul Bottein
01adef6d9f Fix import 2024-11-13 17:46:12 +01:00
Paul Bottein
4a1adf42b8 Merge branch 'dev' into allthebackupchanges 2024-11-13 16:21:12 +01:00
Joakim Sørensen
0c2e62ec91 Early pushout of changes to the backup panel (#22321)
* Eary pushout of changes to the backup panel

* Add location icons

* Path is optional

* Set backupSlug from route

* No need for subscription mixin

* update

* Reorder

* init details
2024-11-13 16:15:14 +01:00
Joakim Sørensen
2218a7121b Add dialog to upload a backup file (#22405)
* Add dialog to upload a backup file

* Prosess feedback

* Remoe unused definition
2024-11-12 09:13:55 +01:00
596 changed files with 8157 additions and 12283 deletions

View File

@@ -4,12 +4,13 @@
# - released in the last year + current alpha/beta versions
# - Firefox extended support release (ESR)
# - with global utilization at or above 0.5%
# - exclude dead browsers (no security maintenance for 2+ years)
# - must support dynamic import of ES modules
# - exclude browsers no longer being maintained
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
unreleased versions
last 1 year
Firefox ESR
>= 0.5%
>= 0.5% and supports es6-module-dynamic-import
not dead
not KaiOS > 0
not QQAndroid > 0
@@ -19,18 +20,23 @@ not UCAndroid > 0
# Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years + current alpha/beta versionss
# - with global utilization at or above 0.05%
# - exclude dead browsers (no security maintenance for 2+ years)
# - exclude Opera Mini which does not support web sockets
# The lattermost query ensures that support for popular old browsers is not dropped too early
# (e.g. IE 11, Android 4.4, or Samsung 4).
#
# In addition, legacy browsers must support some minimum features that cannot be polyfilled:
# - ES5 (strict mode)
# - web sockets to communicate with backend
# - inline SVG used widely in buttons, widgets, etc.
# - custom events used for most user interactions
# - CSS flexbox used in the majority of the layout
# Nearly all of these are redundant with the above rules.
# As of May 2023, only web sockets must be added to the query.
unreleased versions
last 7 years
>= 0.05%
not dead
not op_mini all
>= 0.05% and supports websockets
[legacy-sw]
# Same as legacy plus supports service workers
unreleased versions
last 7 years
>= 0.05% and supports serviceworkers
not dead
not op_mini all
>= 0.05% and supports websockets and supports serviceworkers

View File

@@ -26,24 +26,18 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Check for duplicate dependencies
run: yarn dedupe --check
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.2.0
uses: actions/cache@v4.1.2
with:
path: |
node_modules/.cache/prettier
@@ -66,19 +60,11 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
@@ -92,19 +78,11 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-app
@@ -124,19 +102,11 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.1.14
uses: relative-ci/agent-action@v2.1.13
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -81,61 +81,3 @@ jobs:
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}
requirements: "requirements.txt"
release-landing-page:
name: Release landing-page frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build landing-page
run: landing-page/script/build_landing_page
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@v2.1.0
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
release-supervisor:
name: Release supervisor frontend
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
permissions:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- name: Install dependencies
run: yarn install
- name: Download Translations
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build supervisor
run: hassio/script/build_hassio
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@v2.1.0
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

3
.gitignore vendored
View File

@@ -50,6 +50,3 @@ src/cast/dev_const.ts
# Jetbrains
/.idea/
# test coverage
test/coverage/

View File

@@ -4,7 +4,6 @@
"esbenp.prettier-vscode",
"runem.lit-plugin",
"github.vscode-pull-request-github",
"eamodio.gitlens",
"vitest.explorer"
"eamodio.gitlens"
]
}

32
.vscode/tasks.json vendored
View File

@@ -100,38 +100,6 @@
"instanceLimit": 1
}
},
{
"label": "Develop Landing Page",
"type": "gulp",
"task": "develop-landing-page",
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": "build",
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Demo",
"type": "gulp",

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.3.cjs
yarnPath: .yarn/releases/yarn-4.5.1.cjs

View File

@@ -147,7 +147,6 @@ const polyfillMap = {
...Object.fromEntries(
[
"DateTimeFormat",
"DurationFormat",
"DisplayNames",
"ListFormat",
"NumberFormat",

View File

@@ -53,11 +53,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__SUPERVISOR__: false,
__BACKWARDS_COMPAT__: false,
__STATIC_PATH__: "/static/",
__HASS_URL__: `\`${
"HASS_URL" in process.env
? process.env["HASS_URL"]
: "${location.protocol}//${location.host}"
}\``,
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
),
@@ -157,6 +152,7 @@ module.exports.babelOptions = ({
exclude: [
// \\ for Windows, / for Mac OS and Linux
/node_modules[\\/]core-js/,
/node_modules[\\/]webpack[\\/]buildin/,
],
sourceMaps: !isTestBuild,
overrides: [
@@ -331,17 +327,4 @@ module.exports.config = {
},
};
},
landingPage({ isProdBuild, latestBuild }) {
return {
name: "landing-page" + nameSuffix(latestBuild),
entry: {
entrypoint: path.resolve(paths.landingPage_dir, "src/entrypoint.js"),
},
outputPath: outputPath(paths.landingPage_output_root, latestBuild),
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
};
},
};

View File

@@ -2,22 +2,23 @@ const fs = require("fs");
const path = require("path");
const paths = require("./paths.cjs");
const isTrue = (value) => value === "1" || value?.toLowerCase() === "true";
module.exports = {
useWDS() {
return process.env.WDS === "1";
},
isProdBuild() {
return (
process.env.NODE_ENV === "production" || module.exports.isStatsBuild()
);
},
isStatsBuild() {
return isTrue(process.env.STATS);
return process.env.STATS === "1";
},
isTestBuild() {
return isTrue(process.env.IS_TEST);
return process.env.IS_TEST === "true";
},
isNetlify() {
return isTrue(process.env.NETLIFY);
return process.env.NETLIFY === "true";
},
version() {
const version = fs
@@ -29,6 +30,6 @@ module.exports = {
return version[1];
},
isDevContainer() {
return isTrue(process.env.DEV_CONTAINER);
return process.env.DEV_CONTAINER === "1";
},
};

View File

@@ -8,7 +8,8 @@ import "./gen-icons-json.js";
import "./locale-data.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
import "./wds.js";
import "./webpack.js";
gulp.task(
"develop-app",
@@ -25,7 +26,7 @@ gulp.task(
"build-locale-data"
),
"copy-static-app",
"rspack-watch-app"
env.useWDS() ? "wds-watch-app" : "webpack-watch-app"
)
);
@@ -38,20 +39,9 @@ gulp.task(
"clean",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-app",
"rspack-prod-app",
"webpack-prod-app",
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
// Don't compress running tests
...(env.isTestBuild() || env.isStatsBuild() ? [] : ["compress-app"])
)
);
gulp.task(
"analyze-app",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-app"
...(env.isTestBuild() ? [] : ["compress-app"])
)
);

View File

@@ -4,7 +4,7 @@ import "./entry-html.js";
import "./gather-static.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
import "./webpack.js";
gulp.task(
"develop-cast",
@@ -17,7 +17,7 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"gen-pages-cast-dev",
"rspack-dev-server-cast"
"webpack-dev-server-cast"
)
);
@@ -31,7 +31,7 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"rspack-prod-cast",
"webpack-prod-cast",
"gen-pages-cast-prod"
)
);

View File

@@ -38,14 +38,3 @@ gulp.task(
])
)
);
gulp.task(
"clean-landing-page",
gulp.parallel("clean-translations", async () =>
deleteSync([
paths.landingPage_output_root,
paths.landingPage_build,
paths.build_dir,
])
)
);

View File

@@ -3,6 +3,7 @@
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs";
const filesGlob = "*.{js,json,css,svg,xml}";
@@ -12,6 +13,7 @@ const brotliOptions = {
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
},
};
const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
gulp
@@ -27,6 +29,20 @@ const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
].filter(Boolean),
{ base: rootDir }
)
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(rootDir));
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
@@ -36,5 +52,17 @@ const compressHassioBrotli = () =>
false
);
gulp.task("compress-app", compressAppBrotli);
gulp.task("compress-hassio", compressHassioBrotli);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(
"compress-hassio",
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
);

View File

@@ -5,7 +5,7 @@ import "./gather-static.js";
import "./gen-icons-json.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
import "./webpack.js";
gulp.task(
"develop-demo",
@@ -22,7 +22,7 @@ gulp.task(
"build-locale-data"
),
"copy-static-demo",
"rspack-dev-server-demo"
"webpack-dev-server-demo"
)
);
@@ -37,18 +37,7 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo",
"rspack-prod-demo",
"webpack-prod-demo",
"gen-pages-demo-prod"
)
);
gulp.task(
"analyze-demo",
gulp.series(
async function setEnv() {
process.env.STATS = "1";
},
"clean",
"rspack-prod-demo"
)
);

View File

@@ -11,6 +11,7 @@ import { minify } from "html-minifier-terser";
import template from "lodash.template";
import { dirname, extname, resolve } from "node:path";
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
import env from "../env.cjs";
import paths from "../paths.cjs";
// macOS companion app has no way to obtain the Safari version used by WKWebView,
@@ -55,6 +56,7 @@ const getCommonTemplateVars = () => {
{ ignorePatch: true, allowHigherVersions: true }
);
return {
useWDS: env.useWDS(),
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
};
};
@@ -90,11 +92,13 @@ const minifyHtml = (content, ext) => {
};
// Function to generate a dev task for each project's configuration
// Note Currently WDS paths are hard-coded to only work for app
const genPagesDevTask =
(
pageEntries,
inputRoot,
outputRoot,
useWDS = false,
inputSub = "src/html",
publicRoot = ""
) =>
@@ -105,13 +109,17 @@ const genPagesDevTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
latestEntryJS: entries.map(
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
latestEntryJS: entries.map((entry) =>
useWDS
? `http://localhost:8000/src/entrypoints/${entry}.ts`
: `${publicRoot}/frontend_latest/${entry}.js`
),
es5EntryJS: entries.map(
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
),
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
latestCustomPanelJS: useWDS
? "http://localhost:8000/src/entrypoints/custom-panel.ts"
: `${publicRoot}/frontend_latest/custom-panel.js`,
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
}
);
@@ -168,7 +176,12 @@ const APP_PAGE_ENTRIES = {
gulp.task(
"gen-pages-app-dev",
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
genPagesDevTask(
APP_PAGE_ENTRIES,
paths.polymer_dir,
paths.app_output_root,
env.useWDS()
)
);
gulp.task(
@@ -244,28 +257,6 @@ gulp.task(
)
);
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
gulp.task(
"gen-pages-landing-page-dev",
genPagesDevTask(
LANDING_PAGE_PAGE_ENTRIES,
paths.landingPage_dir,
paths.landingPage_output_root
)
);
gulp.task(
"gen-pages-landing-page-prod",
genPagesProdTask(
LANDING_PAGE_PAGE_ENTRIES,
paths.landingPage_dir,
paths.landingPage_output_root,
paths.landingPage_output_latest,
paths.landingPage_output_es5
)
);
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task(
@@ -274,6 +265,7 @@ gulp.task(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
undefined,
"src",
paths.hassio_publicPath
)

View File

@@ -66,7 +66,7 @@ gulp.task("fetch-nightly-translations", async function () {
tokenAuth = JSON.parse(await readFile(TOKEN_FILE, "utf-8"));
} catch {
if (!allowTokenSetup) {
console.log("No token found so build will continue with English only");
console.log("No token found so build wil continue with English only");
return;
}
const auth = createOAuthDeviceAuth({

View File

@@ -11,7 +11,7 @@ import "./gather-static.js";
import "./gen-icons-json.js";
import "./service-worker.js";
import "./translations.js";
import "./rspack.js";
import "./webpack.js";
gulp.task("gather-gallery-pages", async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages");
@@ -156,7 +156,7 @@ gulp.task(
"copy-static-gallery",
"gen-pages-gallery-dev",
gulp.parallel(
"rspack-dev-server-gallery",
"webpack-dev-server-gallery",
async function watchMarkdownFiles() {
gulp.watch(
[
@@ -185,7 +185,7 @@ gulp.task(
"gather-gallery-pages"
),
"copy-static-gallery",
"rspack-prod-gallery",
"webpack-prod-gallery",
"gen-pages-gallery-prod"
)
);

View File

@@ -125,11 +125,6 @@ gulp.task("copy-translations-supervisor", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-landing-page", async () => {
const staticDir = paths.landingPage_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyLocaleData(staticDir);
@@ -204,14 +199,3 @@ gulp.task("copy-static-gallery", async () => {
copyLocaleData(paths.gallery_output_static);
copyMdiIcons(paths.gallery_output_static);
});
gulp.task("copy-static-landing-page", async () => {
// Copy landing-page static files
fs.copySync(
path.resolve(paths.landingPage_dir, "public"),
paths.landingPage_output_root
);
copyFonts(paths.landingPage_output_static);
copyTranslations(paths.landingPage_output_static);
});

View File

@@ -6,7 +6,7 @@ import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
import "./webpack.js";
gulp.task(
"develop-hassio",
@@ -21,7 +21,7 @@ gulp.task(
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-watch-hassio"
"webpack-watch-hassio"
)
);
@@ -37,7 +37,7 @@ gulp.task(
"copy-translations-supervisor",
"build-locale-data",
"copy-static-supervisor",
"rspack-prod-hassio",
"webpack-prod-hassio",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])

View File

@@ -1,41 +0,0 @@
import gulp from "gulp";
import "./clean.js";
import "./compress.js";
import "./entry-html.js";
import "./gather-static.js";
import "./gen-icons-json.js";
import "./translations.js";
import "./rspack.js";
gulp.task(
"develop-landing-page",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "development";
},
"clean-landing-page",
"translations-enable-merge-backend",
"build-landing-page-translations",
"copy-translations-landing-page",
"build-locale-data",
"copy-static-landing-page",
"gen-pages-landing-page-dev",
"rspack-watch-landing-page"
)
);
gulp.task(
"build-landing-page",
gulp.series(
async function setEnv() {
process.env.NODE_ENV = "production";
},
"clean-landing-page",
"build-landing-page-translations",
"copy-translations-landing-page",
"build-locale-data",
"copy-static-landing-page",
"rspack-prod-landing-page",
"gen-pages-landing-page-prod"
)
);

View File

@@ -172,14 +172,12 @@ const createMasterTranslation = () =>
const FRAGMENTS = ["base"];
const setFragment = (fragment) => async () => {
FRAGMENTS[0] = fragment;
const toggleSupervisorFragment = async () => {
FRAGMENTS[0] = "supervisor";
};
const panelFragment = (fragment) =>
fragment !== "base" &&
fragment !== "supervisor" &&
fragment !== "landing-page";
fragment !== "base" && fragment !== "supervisor";
const HASHES = new Map();
@@ -226,9 +224,6 @@ const createTranslations = async () => {
case "supervisor":
// Supervisor key is at the top level
return [flatten(data.supervisor), ""];
case "landing-page":
// landing-page key is at the top level
return [flatten(data["landing-page"]), ""];
default:
// Create a fragment with only the given panel
return [
@@ -327,10 +322,5 @@ gulp.task(
gulp.task(
"build-supervisor-translations",
gulp.series(setFragment("supervisor"), "build-translations")
);
gulp.task(
"build-landing-page-translations",
gulp.series(setFragment("landing-page"), "build-translations")
gulp.series(toggleSupervisorFragment, "build-translations")
);

10
build-scripts/gulp/wds.js Normal file
View File

@@ -0,0 +1,10 @@
import gulp from "gulp";
import { startDevServer } from "@web/dev-server";
gulp.task("wds-watch-app", async () => {
startDevServer({
config: {
watch: true,
},
});
});

View File

@@ -1,11 +1,11 @@
// Tasks to run rspack.
// Tasks to run webpack.
import fs from "fs";
import path from "path";
import log from "fancy-log";
import gulp from "gulp";
import rspack from "@rspack/core";
import { RspackDevServer } from "@rspack/dev-server";
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import env from "../env.cjs";
import paths from "../paths.cjs";
import {
@@ -14,8 +14,7 @@ import {
createDemoConfig,
createGalleryConfig,
createHassioConfig,
createLandingPageConfig,
} from "../rspack.cjs";
} from "../webpack.cjs";
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),
@@ -31,7 +30,7 @@ const isWsl =
/**
* @param {{
* compiler: import("@rspack/core").Compiler,
* compiler: import("webpack").Compiler,
* contentBase: string,
* port: number,
* listenHost?: string
@@ -42,13 +41,12 @@ const runDevServer = async ({
contentBase,
port,
listenHost = undefined,
proxy = undefined,
}) => {
if (listenHost === undefined) {
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new RspackDevServer(
const server = new WebpackDevServer(
{
hot: false,
open: true,
@@ -58,14 +56,13 @@ const runDevServer = async ({
directory: contentBase,
watch: true,
},
proxy,
},
compiler
);
await server.start();
// Server listening
log("[rspack-dev-server]", `Project is running at http://localhost:${port}`);
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
};
const doneHandler = (done) => (err, stats) => {
@@ -90,16 +87,16 @@ const doneHandler = (done) => (err, stats) => {
const prodBuild = (conf) =>
new Promise((resolve) => {
rspack(
webpack(
conf,
// Resolve promise when done. Because we pass a callback, rspack closes itself
// Resolve promise when done. Because we pass a callback, webpack closes itself
doneHandler(resolve)
);
});
gulp.task("rspack-watch-app", () => {
gulp.task("webpack-watch-app", () => {
// This command will run forever because we don't close compiler
rspack(
webpack(
process.env.ES5
? bothBuilds(createAppConfig, { isProdBuild: false })
: createAppConfig({ isProdBuild: false, latestBuild: true })
@@ -110,7 +107,7 @@ gulp.task("rspack-watch-app", () => {
);
});
gulp.task("rspack-prod-app", () =>
gulp.task("webpack-prod-app", () =>
prodBuild(
bothBuilds(createAppConfig, {
isProdBuild: true,
@@ -120,9 +117,9 @@ gulp.task("rspack-prod-app", () =>
)
);
gulp.task("rspack-dev-server-demo", () =>
gulp.task("webpack-dev-server-demo", () =>
runDevServer({
compiler: rspack(
compiler: webpack(
createDemoConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.demo_output_root,
@@ -130,18 +127,17 @@ gulp.task("rspack-dev-server-demo", () =>
})
);
gulp.task("rspack-prod-demo", () =>
gulp.task("webpack-prod-demo", () =>
prodBuild(
bothBuilds(createDemoConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
})
)
);
gulp.task("rspack-dev-server-cast", () =>
gulp.task("webpack-dev-server-cast", () =>
runDevServer({
compiler: rspack(
compiler: webpack(
createCastConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.cast_output_root,
@@ -151,7 +147,7 @@ gulp.task("rspack-dev-server-cast", () =>
})
);
gulp.task("rspack-prod-cast", () =>
gulp.task("webpack-prod-cast", () =>
prodBuild(
bothBuilds(createCastConfig, {
isProdBuild: true,
@@ -159,9 +155,9 @@ gulp.task("rspack-prod-cast", () =>
)
);
gulp.task("rspack-watch-hassio", () => {
gulp.task("webpack-watch-hassio", () => {
// This command will run forever because we don't close compiler
rspack(
webpack(
createHassioConfig({
isProdBuild: false,
latestBuild: true,
@@ -174,7 +170,7 @@ gulp.task("rspack-watch-hassio", () => {
);
});
gulp.task("rspack-prod-hassio", () =>
gulp.task("webpack-prod-hassio", () =>
prodBuild(
bothBuilds(createHassioConfig, {
isProdBuild: true,
@@ -184,9 +180,9 @@ gulp.task("rspack-prod-hassio", () =>
)
);
gulp.task("rspack-dev-server-gallery", () =>
gulp.task("webpack-dev-server-gallery", () =>
runDevServer({
compiler: rspack(
compiler: webpack(
createGalleryConfig({ isProdBuild: false, latestBuild: true })
),
contentBase: paths.gallery_output_root,
@@ -195,7 +191,7 @@ gulp.task("rspack-dev-server-gallery", () =>
})
);
gulp.task("rspack-prod-gallery", () =>
gulp.task("webpack-prod-gallery", () =>
prodBuild(
createGalleryConfig({
isProdBuild: true,
@@ -203,30 +199,3 @@ gulp.task("rspack-prod-gallery", () =>
})
)
);
gulp.task("rspack-watch-landing-page", () => {
// This command will run forever because we don't close compiler
rspack(
process.env.ES5
? bothBuilds(createLandingPageConfig, { isProdBuild: false })
: createLandingPageConfig({ isProdBuild: false, latestBuild: true })
).watch({ poll: isWsl }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series(
"build-landing-page-translations",
"copy-translations-landing-page"
)
);
});
gulp.task("rspack-prod-landing-page", () =>
prodBuild(
bothBuilds(createLandingPageConfig, {
isProdBuild: true,
isStatsBuild: env.isStatsBuild(),
isTestBuild: env.isTestBuild(),
})
)
);

View File

@@ -33,22 +33,6 @@ module.exports = {
),
gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"),
landingPage_dir: path.resolve(__dirname, "../landing-page"),
landingPage_build: path.resolve(__dirname, "../landing-page/build"),
landingPage_output_root: path.resolve(__dirname, "../landing-page/dist"),
landingPage_output_latest: path.resolve(
__dirname,
"../landing-page/dist/frontend_latest"
),
landingPage_output_es5: path.resolve(
__dirname,
"../landing-page/dist/frontend_es5"
),
landingPage_output_static: path.resolve(
__dirname,
"../landing-page/dist/static"
),
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),

View File

@@ -1,13 +1,16 @@
const { existsSync } = require("fs");
const path = require("path");
const rspack = require("@rspack/core");
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
const webpack = require("webpack");
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar/rspack");
const WebpackBar = require("webpackbar");
const {
TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@@ -25,7 +28,7 @@ class LogStartCompilePlugin {
}
}
const createRspackConfig = ({
const createWebpackConfig = ({
name,
entry,
outputPath,
@@ -99,18 +102,13 @@ const createRspackConfig = ({
splitChunks: {
// Disable splitting for web workers and worklets because imports of
// external chunks are broken for:
chunks: !isProdBuild
? // improve incremental build speed, but blows up bundle size
new RegExp(
`^(?!(${Object.keys(entry).join("|")}|.*work(?:er|let))$)`
)
: // - ESM output: https://github.com/webpack/webpack/issues/17014
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
(chunk) =>
!chunk.canBeInitial() &&
!new RegExp(
`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`
).test(chunk.name),
// - ESM output: https://github.com/webpack/webpack/issues/17014
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
chunks: (chunk) =>
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
},
},
plugins: [
@@ -119,10 +117,10 @@ const createRspackConfig = ({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),
new rspack.DefinePlugin(
new webpack.DefinePlugin(
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
),
new rspack.IgnorePlugin({
new webpack.IgnorePlugin({
checkResource(resource, context) {
// Only use ignore to intercept imports that we don't control
// inside node_module dependencies.
@@ -154,7 +152,7 @@ const createRspackConfig = ({
);
},
}),
new rspack.NormalModuleReplacementPlugin(
new webpack.NormalModuleReplacementPlugin(
new RegExp(
bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|")
),
@@ -170,14 +168,10 @@ const createRspackConfig = ({
stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)),
}),
isProdBuild &&
isStatsBuild &&
new RsdoctorRspackPlugin({
reportDir: path.join(paths.build_dir, "rsdoctor"),
features: ["plugins", "bundle"],
supports: {
generateTileGraph: true,
},
!latestBuild &&
new TransformAsyncModulesPlugin({
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
}),
].filter(Boolean),
resolve: {
@@ -216,6 +210,8 @@ const createRspackConfig = ({
isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
crossOriginLoading: "use-credentials",
hashFunction: "xxhash64",
hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url
path: outputPath,
publicPath,
// To silence warning in worker plugin
@@ -257,17 +253,17 @@ const createAppConfig = ({
isStatsBuild,
isTestBuild,
}) =>
createRspackConfig(
createWebpackConfig(
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
);
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
createRspackConfig(
createWebpackConfig(
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
);
const createCastConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
createWebpackConfig(bundle.config.cast({ isProdBuild, latestBuild }));
const createHassioConfig = ({
isProdBuild,
@@ -275,7 +271,7 @@ const createHassioConfig = ({
isStatsBuild,
isTestBuild,
}) =>
createRspackConfig(
createWebpackConfig(
bundle.config.hassio({
isProdBuild,
latestBuild,
@@ -285,10 +281,7 @@ const createHassioConfig = ({
);
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
module.exports = {
createAppConfig,
@@ -296,6 +289,5 @@ module.exports = {
createCastConfig,
createHassioConfig,
createGalleryConfig,
createRspackConfig,
createLandingPageConfig,
createWebpackConfig,
};

View File

@@ -25,9 +25,9 @@ class HcLovelace extends LitElement {
@property({ attribute: false })
public lovelaceConfig!: LovelaceConfig;
@property({ attribute: false }) public viewPath?: string | number | null;
@property() public viewPath?: string | number | null;
@property({ attribute: false }) public urlPath: string | null = null;
@property() public urlPath: string | null = null;
protected render(): TemplateResult {
const index = this._viewIndex;

View File

@@ -144,10 +144,10 @@ export class HcMain extends HassElement {
}
if (senderId) {
this._sendMessage(senderId, status);
this.sendMessage(senderId, status);
} else {
for (const sender of castContext.getSenders()) {
this._sendMessage(sender.id, status);
this.sendMessage(sender.id, status);
}
}
}
@@ -164,10 +164,10 @@ export class HcMain extends HassElement {
};
if (senderId) {
this._sendMessage(senderId, error);
this.sendMessage(senderId, error);
} else {
for (const sender of castContext.getSenders()) {
this._sendMessage(sender.id, error);
this.sendMessage(sender.id, error);
}
}
}
@@ -394,7 +394,7 @@ export class HcMain extends HassElement {
}
}
private _sendMessage(senderId: string, response: any) {
private sendMessage(senderId: string, response: any) {
castContext.sendCustomMessage(CAST_NS, senderId, response);
}
}

8
cast/webpack.config.js Normal file
View File

@@ -0,0 +1,8 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createCastConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -4,6 +4,11 @@
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp analyze-demo
export STATS=1
statsfile="compilation-stats-demo.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile dist/frontend_latest
rm -f $statsfile

View File

@@ -46,6 +46,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
this.requestUpdate();
});
mgr.castContext.addEventListener(
// eslint-disable-next-line no-undef
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => {
// On Android, opening a new session always results in SESSION_RESUMED.

View File

@@ -26,7 +26,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
@state() private _switching = false;
private _hidden = window.localStorage.getItem("hide_demo_card");
private _hidden = localStorage.hide_demo_card;
public getCardSize() {
return this._hidden ? 0 : 2;

11
demo/webpack.config.js Normal file
View File

@@ -0,0 +1,11 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
// File just used for stats builds
const latestBuild = true;
export default webpack.createDemoConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild,
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
@@ -7,10 +6,10 @@ import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: _dirname,
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
@@ -58,7 +57,7 @@ export default [
settings: {
"import/resolver": {
webpack: {
config: "./rspack.config.cjs",
config: "./webpack.config.cjs",
},
},
},
@@ -114,10 +113,12 @@ export default [
"@typescript-eslint/no-shadow": ["error"],
"@typescript-eslint/naming-convention": [
"error",
"off",
{
selector: ["objectLiteralProperty", "objectLiteralMethod"],
format: null,
selector: "default",
format: ["camelCase", "snake_case"],
leadingUnderscore: "allow",
trailingUnderscore: "allow",
},
{
selector: ["variable"],
@@ -125,27 +126,10 @@ export default [
leadingUnderscore: "allow",
trailingUnderscore: "allow",
},
{
selector: ["variable"],
modifiers: ["exported"],
format: ["camelCase", "PascalCase", "UPPER_CASE"],
},
{
selector: "typeLike",
format: ["PascalCase"],
},
{
selector: "method",
modifiers: ["public"],
format: ["camelCase"],
leadingUnderscore: "forbid",
},
{
selector: "method",
modifiers: ["private"],
format: ["camelCase"],
leadingUnderscore: "require",
},
],
"@typescript-eslint/no-unused-vars": "off",
@@ -162,16 +146,16 @@ export default [
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-names": "error",
"lit/attribute-names": "warn",
"lit/attribute-value-entities": "off",
"lit/no-template-map": "off",
"lit/no-native-attributes": "error",
"lit/no-this-assign-in-render": "error",
"lit/no-native-attributes": "warn",
"lit/no-this-assign-in-render": "warn",
"lit-a11y/click-events-have-key-events": ["off"],
"lit-a11y/no-autofocus": "off",
"lit-a11y/alt-text": "error",
"lit-a11y/anchor-is-valid": "error",
"lit-a11y/role-has-required-aria-attrs": "error",
"lit-a11y/alt-text": "warn",
"lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
},

View File

@@ -9,7 +9,6 @@ import "../../../src/components/ha-card";
@customElement("demo-black-white-row")
class DemoBlackWhiteRow extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property() title!: string;
@property() value?: any;

View File

@@ -18,8 +18,7 @@ class DemoCard extends LitElement {
@property({ attribute: false }) public config!: DemoCardConfig;
@property({ attribute: "show-config", type: Boolean })
public showConfig = false;
@property({ type: Boolean }) public showConfig = false;
@state() private _size?: number;

View File

@@ -44,11 +44,11 @@ class DemoCards extends LitElement {
`;
}
private _showConfigToggled(ev) {
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
private _darkThemeToggled(ev) {
_darkThemeToggled(ev) {
applyThemesOnElement(this._container, { themes: {} } as any, "default", {
dark: ev.target.checked,
});

View File

@@ -10,10 +10,9 @@ import type { HomeAssistant } from "../../../src/types";
class DemoMoreInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@property() public entityId!: string;
@property({ attribute: "show-config", type: Boolean })
public showConfig = false;
@property({ type: Boolean }) public showConfig = false;
render() {
const state = this._getState(this.entityId, this.hass.states);
@@ -24,7 +23,7 @@ class DemoMoreInfo extends LitElement {
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
inDialog
></state-card-content>
<more-info-content

View File

@@ -58,11 +58,11 @@ class DemoMoreInfos extends LitElement {
}
`;
private _showConfigToggled(ev) {
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
private _darkThemeToggled(ev) {
_darkThemeToggled(ev) {
applyThemesOnElement(
this.shadowRoot!.querySelector("#container"),
{

View File

@@ -182,7 +182,7 @@ class HaGallery extends LitElement {
}
}
private _menuTapped() {
_menuTapped() {
this._drawer.open = !this._drawer.open;
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable lit/no-template-arrow */
import type { TemplateResult } from "lit";
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators";
@@ -14,6 +15,7 @@ import { HaDelayAction } from "../../../../src/panels/config/automation/action/t
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-activate_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";
@@ -31,6 +33,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
@@ -63,6 +66,11 @@ class DemoHaAutomationEditorAction extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -87,7 +95,7 @@ class DemoHaAutomationEditorAction extends LitElement {
.actions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${this._handleValueChange}
@value-changed=${valueChanged}
></ha-automation-action>
`
)}
@@ -97,12 +105,6 @@ class DemoHaAutomationEditorAction extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable lit/no-template-arrow */
import type { TemplateResult } from "lit";
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators";
@@ -103,6 +104,11 @@ export class DemoAutomationEditorCondition extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -127,7 +133,7 @@ export class DemoAutomationEditorCondition extends LitElement {
.conditions=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${this._handleValueChange}
@value-changed=${valueChanged}
></ha-automation-condition>
`
)}
@@ -137,12 +143,6 @@ export class DemoAutomationEditorCondition extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -149,6 +149,11 @@ export class DemoAutomationEditorTrigger extends LitElement {
}
protected render(): TemplateResult {
const valueChanged = (ev) => {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
};
return html`
<div class="options">
<ha-formfield label="Disabled">
@@ -173,7 +178,7 @@ export class DemoAutomationEditorTrigger extends LitElement {
.triggers=${this.data[sampleIdx]}
.sampleIdx=${sampleIdx}
.disabled=${this._disabled}
@value-changed=${this._handleValueChange}
@value-changed=${valueChanged}
></ha-automation-trigger>
`
)}
@@ -183,12 +188,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
`;
}
private _handleValueChange(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -31,17 +31,22 @@ export class DemoAutomationTrace extends LitElement {
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${this._handleGraphNodeSelected}
.sampleIdx=${idx}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
></hat-script-graph>
<hat-trace-timeline
allow-pick
allowPick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${this._handleTimelineValueChanged}
.sampleIdx=${idx}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
@@ -58,16 +63,6 @@ export class DemoAutomationTrace extends LitElement {
hass.updateTranslations("config", "en");
}
private _handleTimelineValueChanged(ev) {
const sampleIdx = ev.target.sampleIdx;
this._selected = { ...this._selected, [sampleIdx]: ev.detail.value };
}
private _handleGraphNodeSelected(ev) {
const sampleIdx = ev.target.sampleIdx;
this._selected = { ...this._selected, [sampleIdx]: ev.detail.path };
}
static get styles() {
return css`
ha-card {

View File

@@ -489,8 +489,14 @@ class DemoHaForm extends LitElement {
.title=${info.title}
.value=${this.data[idx]}
.disabled=${this.disabled[idx]}
@submitted=${this._handleSubmit}
.sampleIdx=${idx}
@submitted=${() => {
this.disabled[idx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[idx] = false;
this.requestUpdate();
}, 2000);
}}
>
${["light", "dark"].map(
(slot) => html`
@@ -505,8 +511,10 @@ class DemoHaForm extends LitElement {
.computeLabel=${(schema) =>
translations[schema.name] || schema.name}
.computeHelper=${() => "Helper text"}
@value-changed=${this._handleValueChanged}
.sampleIdx=${idx}
@value-changed=${(e) => {
this.data[idx] = e.detail.value;
this.requestUpdate();
}}
></ha-form>
`
)}
@@ -515,22 +523,6 @@ class DemoHaForm extends LitElement {
})}
`;
}
private _handleValueChanged(ev) {
const sampleIdx = ev.target.sampleIdx;
this.data[sampleIdx] = ev.detail.value;
this.requestUpdate();
}
private _handleSubmit(ev) {
const sampleIdx = ev.target.sampleIdx;
this.disabled[sampleIdx] = true;
this.requestUpdate();
setTimeout(() => {
this.disabled[sampleIdx] = false;
this.requestUpdate();
}, 2000);
}
}
declare global {

View File

@@ -1,3 +1,4 @@
/* eslint-disable lit/no-template-arrow */
import "@material/mwc-button";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
@@ -590,6 +591,13 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
</div>
${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}>
${["light", "dark"].map((slot) =>
@@ -606,8 +614,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.value=${data[key] ?? value!.default}
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${this._handleValueChanged}
.sampleIdx=${idx}
@value-changed=${valueChanged}
.helper=${this._helper ? "Helper text" : undefined}
></ha-selector>
</ha-settings-row>
@@ -620,15 +627,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
`;
}
private _handleValueChanged(ev) {
const idx = ev.target.sampleIdx;
this.data[idx] = {
...this.data[idx],
[ev.target.key]: ev.detail.value,
};
this.requestUpdate();
}
private _handleOptionChange(ev) {
this[`_${ev.target.name}`] = ev.target.checked;
}

View File

@@ -86,11 +86,9 @@ const ENTITIES = [
friendly_name: "Sensibo purifier",
fan_modes: ["low", "high"],
fan_mode: "low",
swing_modes: ["both", "rangefull", "off"],
swing_mode: "rangefull",
swing_horizontal_modes: ["both", "rangefull", "off"],
swing_horizontal_mode: "both",
supported_features: 553,
swing_modes: ["on", "off", "both", "vertical", "horizontal"],
swing_mode: "vertical",
supported_features: 41,
}),
getEntity("climate", "unavailable", "unavailable", {
supported_features: 43,
@@ -190,13 +188,11 @@ const CONFIGS = [
- type: climate-swing-modes
style: icons
swing_modes:
- 'both'
- 'rangefull'
- 'on'
- 'off'
swing_horizontal_modes:
- 'both'
- 'rangefull'
- 'off'
- 'vertical'
- 'horizontal'
`,
},
{

View File

@@ -4,7 +4,6 @@ import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { MediaPlayerEntityFeature } from "../../../../src/data/media-player";
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -29,10 +28,6 @@ const ENTITIES = [
device_class: "lock",
supported_features: LockEntityFeature.OPEN,
}),
getEntity("media_player", "living_room", "playing", {
friendly_name: "Living room speaker",
supported_features: MediaPlayerEntityFeature.VOLUME_SET,
}),
getEntity("climate", "thermostat", "heat", {
current_temperature: 73,
min_temp: 45,
@@ -83,19 +78,16 @@ const ENTITIES = [
fan_modes: ["on_low", "on_high", "auto_low", "auto_high", "off"],
preset_modes: ["home", "eco", "away"],
swing_modes: ["auto", "1", "2", "3", "off"],
switch_horizontal_modes: ["auto", "4", "5", "6", "off"],
current_temperature: 23,
target_temp_high: 24,
target_temp_low: 21,
fan_mode: "auto_low",
preset_mode: "home",
swing_mode: "auto",
swing_horizontal_mode: "off",
supported_features:
ClimateEntityFeature.TURN_ON +
ClimateEntityFeature.TURN_OFF +
ClimateEntityFeature.SWING_MODE +
ClimateEntityFeature.SWING_HORIZONTAL_MODE +
ClimateEntityFeature.PRESET_MODE +
ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
@@ -202,15 +194,6 @@ const CONFIGS = [
- type: "lock-open-door"
`,
},
{
heading: "Media player volume slider feature",
config: `
- type: tile
entity: media_player.living_room
features:
- type: "media-player-volume-slider"
`,
},
{
heading: "Vacuum commands feature",
config: `

View File

@@ -0,0 +1,8 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createGalleryConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -136,7 +136,7 @@ export class HassioAddonStore extends LitElement {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
@@ -179,7 +179,7 @@ export class HassioAddonStore extends LitElement {
}
}
private _apiCalled(ev) {
private apiCalled(ev) {
if (ev.detail.success) {
this._loadData();
}

View File

@@ -315,7 +315,6 @@ class HassioAddonConfig extends LitElement {
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
});
if (!confirmed) {

View File

@@ -58,7 +58,7 @@ export class HassioBackups extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public isWide = false;
@state() private _selectedBackups: string[] = [];
@@ -74,7 +74,7 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this._fetchBackups();
this.fetchBackups();
}
}
@@ -107,7 +107,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this._fetchBackups();
this.fetchBackups();
}
this._firstUpdatedCalled = true;
}
@@ -198,7 +198,7 @@ export class HassioBackups extends LitElement {
@selection-changed=${this._handleSelectionChanged}
clickable
selectable
has-fab
hasFab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
@@ -280,7 +280,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._fetchBackups();
this.fetchBackups();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -303,13 +303,13 @@ export class HassioBackups extends LitElement {
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
onDelete: () => this.fetchBackups(),
}),
reloadBackup: () => this._fetchBackups(),
reloadBackup: () => this.fetchBackups(),
});
}
private async _fetchBackups() {
private async fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
@@ -323,7 +323,6 @@ export class HassioBackups extends LitElement {
number: this._selectedBackups.length,
}),
confirmText: this.supervisor.localize("backup.delete_backup_confirm"),
destructive: true,
});
if (!confirm) {
@@ -341,7 +340,7 @@ export class HassioBackups extends LitElement {
});
return;
}
await this._fetchBackups();
await this.fetchBackups();
this._dataTable.clearSelection();
}
@@ -350,7 +349,7 @@ export class HassioBackups extends LitElement {
showHassioBackupDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._fetchBackups(),
onDelete: () => this.fetchBackups(),
});
}
@@ -366,7 +365,7 @@ export class HassioBackups extends LitElement {
}
showHassioCreateBackupDialog(this, {
supervisor: this.supervisor!,
onCreate: () => this._fetchBackups(),
onCreate: () => this.fetchBackups(),
});
}

View File

@@ -9,24 +9,23 @@ import type { HomeAssistant } from "../../../src/types";
class HassioCardContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available = true;
@property({ attribute: false }) public showTopbar = false;
@property({ type: Boolean }) public showTopbar = false;
@property({ attribute: false }) public topbarClass?: string;
@property() public topbarClass?: string;
@property({ attribute: false }) public iconTitle?: string;
@property() public iconTitle?: string;
@property({ attribute: false }) public iconClass?: string;
@property() public iconClass?: string;
@property() public icon = mdiHelpCircle;
@property({ attribute: false }) public iconImage?: string;
@property() public iconImage?: string;
protected render(): TemplateResult {
return html`
@@ -36,11 +35,7 @@ class HassioCardContent extends LitElement {
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img
src=${this.iconImage}
.title=${this.iconTitle}
alt=${this.iconTitle ?? ""}
/>
<img src=${this.iconImage} .title=${this.iconTitle} />
<div></div>
</div>
`

View File

@@ -73,24 +73,23 @@ export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public backup?: HassioBackupDetail;
@property({ attribute: false })
public backupType: HassioBackupDetail["type"] = "full";
@property() public backupType: HassioBackupDetail["type"] = "full";
@property({ attribute: false }) public folders?: CheckboxItem[];
@property({ attribute: false }) public addons?: AddonCheckboxItem[];
@property({ attribute: false }) public homeAssistant = false;
@property({ type: Boolean }) public homeAssistant = false;
@property({ attribute: false }) public backupHasPassword = false;
@property({ type: Boolean }) public backupHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property({ attribute: false }) public backupName = "";
@property() public backupName = "";
@property({ attribute: false }) public backupPassword = "";
@property() public backupPassword = "";
@property({ attribute: false }) public confirmBackupPassword = "";
@property() public confirmBackupPassword = "";
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget;
@@ -192,7 +191,7 @@ export class SupervisorBackupContent extends LitElement {
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this._toggleHomeAssistant}
@change=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>`
@@ -278,7 +277,7 @@ export class SupervisorBackupContent extends LitElement {
`;
}
private _toggleHomeAssistant() {
private toggleHomeAssistant() {
this.homeAssistant = !this.homeAssistant;
}

View File

@@ -7,9 +7,9 @@ import "../../../src/components/ha-svg-icon";
class SupervisorFormfieldLabel extends LitElement {
@property({ type: String }) public label!: string;
@property({ attribute: false }) public imageUrl?: string;
@property({ type: String }) public imageUrl?: string;
@property({ attribute: false }) public iconPath?: string;
@property({ type: String }) public iconPath?: string;
@property({ type: String }) public version?: string;

View File

@@ -76,7 +76,7 @@ class HassioDashboard extends LitElement {
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor
has-fab
hasFab
>
<span slot="header">
${this.supervisor.localize(

View File

@@ -302,7 +302,6 @@ class HassioBackupDialog
text: supervisor!.localize("backup.confirm_delete_text"),
confirmText: supervisor!.localize("backup.delete"),
dismissText: supervisor!.localize("backup.cancel"),
destructive: true,
}))
) {
return;

View File

@@ -95,7 +95,7 @@ class HassioDatadiskDialog extends LitElement {
.label=${this.dialogParams.supervisor.localize(
"dialog.datadisk_move.select_device"
)}
@selected=${this._selectDevice}
@selected=${this._select_device}
dialogInitialFocus
>
${this.devices.map(
@@ -137,7 +137,7 @@ class HassioDatadiskDialog extends LitElement {
`;
}
private _selectDevice(ev) {
private _select_device(ev) {
this.selectedDevice = ev.target.value;
}

View File

@@ -12,7 +12,6 @@ import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
class HassioMarkdownDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property() public title!: string;
@property() public content!: string;

View File

@@ -394,7 +394,7 @@ export class DialogHassioNetwork
`;
}
private _toArray(data: string | string[]): string[] {
_toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
@@ -409,7 +409,7 @@ export class DialogHassioNetwork
return data;
}
private _toString(data: string | string[]): string {
_toString(data: string | string[]): string {
if (!data) {
return "";
}

View File

@@ -34,7 +34,7 @@ class HassioIngressView extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public ingressPanel = false;
@property({ type: Boolean }) public ingressPanel = false;
@property({ type: Boolean }) public narrow = false;

View File

@@ -58,10 +58,10 @@ const SUPERVISOR_UPDATE_NAMES = {
supervisor: "Home Assistant Supervisor",
};
type UpdateType = "os" | "supervisor" | "core" | "addon";
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
entry: UpdateType,
entry: updateType,
version: string
): string | undefined => {
if (entry === "addon") {
@@ -99,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: UpdateType;
@state() private _updateType?: updateType;
@state() private _changelogContent?: string;
@@ -222,7 +222,7 @@ class UpdateAvailableCard extends LitElement {
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as UpdateType;
this._updateType = updateType as updateType;
switch (updateType) {
case "addon":

8
hassio/webpack.config.js Normal file
View File

@@ -0,0 +1,8 @@
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
export default webpack.createHassioConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -1,44 +0,0 @@
# Home Assistant OS Landingpage
On initial startup of Home Assistant, HAOS needs to download Home Assistant core before the setup can start.
In this time the [home-assistant/landingpage](https://github.com/home-assistant/landingpage) is serving a "Preparing Home Assistant" page.
## Functionality
- Progress bar to show download
- Show / hide supervisor logs
- Links
- Read our Vision
- Join our community
- Download our app
- DNS issue handler
- if the supervisor is not able to connect to the internet
- Show actions to set dns to google or cloudflare to resolve the issue
- Error handler
- if something with the installation goes wrong, we show the logs
## Develop
It is similar to the core frontend dev.
- frontend repo is building stuff
- landingpage repo can set the frontend repo path and serve the dev frontend
### landingpage dev server
- clone [home-assistant/landingpage](https://github.com/home-assistant/landingpage)
- Add frontend repo as mount to your devcontainer config
- please do not commit this changes, you can remove it after initial dev container build, because the build will keep the options as long as you don't rebuild it.
- `"mounts": ["source=/path/to/hass/frontend,target=/workspaces/frontend,type=bind,consistency=cached"]`
- use the dev container
- start the dev server with following optional env vars:
- `SUPERVISOR_HOST` to have real supervisor data, you can [setup a supervisor remote API access](https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access) and set the host of your supervisor. e.g.: `SUPERVISOR_HOST=192.168.0.20:8888`
- `SUPERVISOR_TOKEN` the supervisor api token you get from the Remote API proxy Addon Logs
- `FRONTEND_PATH` the path inside your container should be `/workspaces/frontend`
- example: `SUPERVISOR_TOKEN=abc123 SUPERVISOR_HOST=192.168.0.20:8888 FRONTEND_PATH=/workspaces/frontend go run main.go http.go mdns.go`
- You can also add this into your devcontainer settings, but then it's not so flexible to change if you want to test something else.
### frontend dev server
- install all dependencies
- run `landing-page/script/develop`

View File

@@ -1,8 +0,0 @@
import rootConfig from "../eslint.config.mjs";
export default [
...rootConfig,
{
rules: {},
},
];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,3 +0,0 @@
<svg width="92" height="136" viewBox="0 0 92 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.38 104.9H3.03C2.24231 104.9 1.48688 105.213 0.929897 105.77C0.372914 106.327 0.0600052 107.082 0.0600052 107.87V132.47C0.0600052 133.258 0.372914 134.013 0.929897 134.57C1.48688 135.127 2.24231 135.44 3.03 135.44H12.36C13.1477 135.44 13.9031 135.127 14.4601 134.57C15.0171 134.013 15.33 133.258 15.33 132.47V120.17H76.39V132.47C76.39 133.258 76.7029 134.013 77.2599 134.57C77.8169 135.127 78.5723 135.44 79.36 135.44H88.69C89.4777 135.44 90.2331 135.127 90.7901 134.57C91.3471 134.013 91.66 133.258 91.66 132.47V107.87C91.66 107.082 91.3471 106.327 90.7901 105.77C90.2331 105.213 89.4777 104.9 88.69 104.9H76.39H76.38ZM50.04 2.24996C47.73 -0.0600439 43.95 -0.0600439 41.65 2.24996L4.25 39.65C1.94 41.96 0.0500031 46.52 0.0500031 49.78V83.7C0.0500031 86.96 2.72 89.64 5.99 89.64H85.7C88.96 89.64 91.64 86.97 91.64 83.7V49.78C91.64 46.52 89.75 41.96 87.44 39.65L50.04 2.24996Z" fill="#18BCF2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1011 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,10 +0,0 @@
<svg width="75" height="79" viewBox="0 0 75 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" fill="url(#paint0_linear_549_34)"/>
<path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_549_34" x1="37.0692" y1="0" x2="37.0692" y2="79" gradientUnits="userSpaceOnUse">
<stop stop-color="#6364FF"/>
<stop offset="1" stop-color="#563ACC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,3 +0,0 @@
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 430 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,9 +0,0 @@
#!/bin/sh
# Run the landing-page
# Stop on errors
set -e
cd "$(dirname "$0")/../.."
./node_modules/.bin/gulp build-landing-page

View File

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

View File

@@ -1,334 +0,0 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map";
import { customElement, property, query, state } from "lit/decorators";
import type {
LandingPageKeys,
LocalizeFunc,
} from "../../../src/common/translations/localize";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-ansi-to-html";
import "../../../src/components/ha-alert";
import type { HaAnsiToHtml } from "../../../src/components/ha-ansi-to-html";
import {
getObserverLogs,
downloadUrl as observerLogsDownloadUrl,
} from "../data/observer";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { fileDownload } from "../../../src/util/file_download";
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
declare global {
interface HASSDomEvents {
"landing-page-error": undefined;
}
}
const SCHEDULE_FETCH_OBSERVER_LOGS = 5;
@customElement("landing-page-logs")
class LandingPageLogs extends LitElement {
@property({ attribute: false })
public localize!: LocalizeFunc<LandingPageKeys>;
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
@query(".logs") private _logElement?: HTMLElement;
@query("#scroll-bottom-marker")
private _scrollBottomMarkerElement?: HTMLElement;
@state() private _show = false;
@state() private _scrolledToBottomController =
new IntersectionController<boolean>(this, {
callback(this: IntersectionController<boolean>, entries) {
return entries[0].isIntersecting;
},
});
@state() private _error = false;
@state() private _newLogsIndicator?: boolean;
@state() private _logLinesCount = 0;
protected render() {
return html`
<div class="actions">
<ha-button @click=${this._toggleLogDetails}>
${this.localize(this._show ? "hide_details" : "show_details")}
</ha-button>
${this._show
? html`<ha-icon-button
.label=${this.localize("logs.download_logs")}
.path=${mdiDownload}
@click=${this._downloadLogs}
></ha-icon-button>`
: nothing}
</div>
${this._error
? html`
<ha-alert
alert-type="error"
.title=${this.localize("logs.fetch_error")}
>
<ha-button @click=${this._startLogStream}>
${this.localize("logs.retry")}
</ha-button>
</ha-alert>
`
: nothing}
<div
class=${classMap({
logs: true,
hidden: !this._show,
})}
>
<ha-ansi-to-html></ha-ansi-to-html>
<div id="scroll-bottom-marker"></div>
</div>
<ha-button
class="new-logs-indicator ${classMap({
visible:
(this._show &&
this._newLogsIndicator &&
!this._scrolledToBottomController.value) ||
false,
})}"
@click=${this._scrollToBottom}
>
<ha-svg-icon .path=${mdiArrowCollapseDown} slot="icon"></ha-svg-icon>
${this.localize("logs.scroll_down_button")}
<ha-svg-icon
.path=${mdiArrowCollapseDown}
slot="trailingIcon"
></ha-svg-icon>
</ha-button>
`;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
this._startLogStream();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
this._newLogsIndicator = false;
}
if (changedProps.has("_show") && this._show) {
this._scrollToBottom();
}
}
private _toggleLogDetails() {
this._show = !this._show;
}
private _scrollToBottom(): void {
if (this._logElement) {
this._newLogsIndicator = false;
this._logElement!.scrollTo(0, this._logElement!.scrollHeight);
}
}
private _displayLogs(logs: string, tempLogLine = "", clear = false): string {
if (clear) {
this._ansiToHtmlElement?.clear();
this._logLinesCount = 0;
}
const showError = ERROR_CHECK.test(logs);
const scrolledToBottom = this._scrolledToBottomController.value;
const lines = `${tempLogLine}${logs}`
.split("\n")
.filter((line) => line.trim() !== "");
// handle edge case where the last line is not complete
if (logs.endsWith("\n")) {
tempLogLine = "";
} else {
tempLogLine = lines.splice(-1, 1)[0];
}
if (lines.length) {
this._ansiToHtmlElement?.parseLinesToColoredPre(lines);
this._logLinesCount += lines.length;
}
if (showError) {
fireEvent(this, "landing-page-error");
this._show = true;
}
if (showError || (scrolledToBottom && this._logElement)) {
this._scrollToBottom();
} else {
this._newLogsIndicator = true;
}
return tempLogLine;
}
private async _startLogStream() {
this._error = false;
this._newLogsIndicator = false;
this._ansiToHtmlElement?.clear();
try {
const response = await getSupervisorLogsFollow();
if (!response.ok || !response.body) {
throw new Error("No stream body found");
}
let tempLogLine = "";
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
// eslint-disable-next-line no-await-in-loop
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
const chunk = decoder.decode(value, { stream: !done });
tempLogLine = this._displayLogs(chunk, tempLogLine);
}
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
// fallback to observerlogs if there is a problem with supervisor
this._loadObserverLogs();
}
}
private _scheduleObserverLogs() {
setTimeout(async () => {
try {
// check if supervisor logs are available
const superVisorLogsResponse = await getSupervisorLogs(1);
if (superVisorLogsResponse.ok) {
this._startLogStream();
return;
}
} catch (err) {
// ignore and continue with observer logs
}
this._loadObserverLogs();
}, SCHEDULE_FETCH_OBSERVER_LOGS * 1000);
}
private async _loadObserverLogs() {
try {
const response = await getObserverLogs();
if (!response.ok) {
throw new Error("Error fetching observer logs");
}
const logs = await response.text();
this._displayLogs(logs, "", true);
this._scheduleObserverLogs();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._error = true;
}
}
private _downloadLogs() {
const timeString = new Date().toISOString().replace(/:/g, "-");
fileDownload(observerLogsDownloadUrl, `observer_${timeString}.log`);
}
static styles = [
css`
:host {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
ha-alert {
width: 100%;
}
.actions {
position: relative;
width: 100%;
text-align: center;
}
.actions ha-icon-button {
position: absolute;
right: 0;
top: -4px;
--icon-primary-color: var(--primary-color);
}
.logs {
width: 100%;
max-height: 300px;
overflow: auto;
border: 1px solid var(--divider-color);
border-radius: 4px;
padding: 4px;
}
.logs.hidden {
display: none;
}
.new-logs-indicator {
--mdc-theme-primary: var(--text-primary-color);
overflow: hidden;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: var(--primary-color);
border-radius: 8px;
transition: height 0.4s ease-out;
display: flex;
justify-content: space-between;
align-items: center;
}
.new-logs-indicator.visible {
height: 24px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"landing-page-logs": LandingPageLogs;
}
}

View File

@@ -1,186 +0,0 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
type CSSResultGroup,
LitElement,
type PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import type {
LandingPageKeys,
LocalizeFunc,
} from "../../../src/common/translations/localize";
import "../../../src/components/ha-button";
import "../../../src/components/ha-alert";
import {
ALTERNATIVE_DNS_SERVERS,
getSupervisorNetworkInfo,
setSupervisorNetworkDns,
} from "../data/supervisor";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
@customElement("landing-page-network")
class LandingPageNetwork extends LitElement {
@property({ attribute: false })
public localize!: LocalizeFunc<LandingPageKeys>;
@state() private _networkIssue = false;
@state() private _getNetworkInfoError = false;
@state() private _dnsPrimaryInterfaceNameservers?: string;
@state() private _dnsPrimaryInterface?: string;
protected render() {
if (!this._networkIssue && !this._getNetworkInfoError) {
return nothing;
}
if (this._getNetworkInfoError) {
return html`
<ha-alert alert-type="error">
<p>${this.localize("network_issue.error_get_network_info")}</p>
</ha-alert>
`;
}
return html`
<ha-alert
alert-type="warning"
.title=${this.localize("network_issue.title")}
>
<p>
${this.localize("network_issue.description", {
dns: this._dnsPrimaryInterfaceNameservers || "?",
})}
</p>
<p>${this.localize("network_issue.resolve_different")}</p>
${!this._dnsPrimaryInterfaceNameservers
? html`
<p>
<b>${this.localize("network_issue.no_primary_interface")} </b>
</p>
`
: nothing}
<div class="actions">
${ALTERNATIVE_DNS_SERVERS.map(
({ translationKey }, key) =>
html`<ha-button
.index=${key}
.disabled=${!this._dnsPrimaryInterfaceNameservers}
@click=${this._setDns}
>${this.localize(translationKey)}</ha-button
>`
)}
</div>
</ha-alert>
`;
}
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._fetchSupervisorInfo();
}
private _scheduleFetchSupervisorInfo() {
setTimeout(
() => this._fetchSupervisorInfo(),
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
);
}
private async _fetchSupervisorInfo() {
let data;
try {
const response = await getSupervisorNetworkInfo();
if (!response.ok) {
throw new Error("Failed to fetch network info");
}
({ data } = await response.json());
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
this._getNetworkInfoError = true;
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
return;
}
this._getNetworkInfoError = false;
const primaryInterface = data.interfaces.find(
(intf) => intf.primary && intf.enabled
);
if (primaryInterface) {
this._dnsPrimaryInterfaceNameservers = [
...(primaryInterface.ipv4?.nameservers || []),
...(primaryInterface.ipv6?.nameservers || []),
].join(", ");
this._dnsPrimaryInterface = primaryInterface.interface;
} else {
this._dnsPrimaryInterfaceNameservers = undefined;
this._dnsPrimaryInterface = undefined;
}
if (!data.host_internet) {
this._networkIssue = true;
} else {
this._networkIssue = false;
}
fireEvent(this, "value-changed", {
value: this._networkIssue,
});
this._scheduleFetchSupervisorInfo();
}
private async _setDns(ev) {
const index = ev.target?.index;
try {
const response = await setSupervisorNetworkDns(
index,
this._dnsPrimaryInterface!
);
if (!response.ok) {
throw new Error("Failed to set DNS");
}
this._networkIssue = false;
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
showAlertDialog(this, {
title: this.localize("network_issue.failed"),
warning: true,
text: `${this.localize(
"network_issue.set_dns_failed"
)}${err?.message ? ` ${this.localize("network_issue.error")}: ${err.message}` : ""}`,
confirmText: this.localize("network_issue.close"),
});
}
}
static get styles(): CSSResultGroup {
return [
css`
.actions {
display: flex;
justify-content: flex-end;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"landing-page-network": LandingPageNetwork;
}
}

View File

@@ -1,5 +0,0 @@
export async function getObserverLogs() {
return fetch("/observer/logs");
}
export const downloadUrl = "/observer/logs";

View File

@@ -1,59 +0,0 @@
import type { LandingPageKeys } from "../../../src/common/translations/localize";
export const ALTERNATIVE_DNS_SERVERS: {
ipv4: string[];
ipv6: string[];
translationKey: LandingPageKeys;
}[] = [
{
ipv4: ["1.1.1.1", "1.0.0.1"],
ipv6: ["2606:4700:4700::1111", "2606:4700:4700::1001"],
translationKey: "network_issue.use_cloudflare",
},
{
ipv4: ["8.8.8.8", "8.8.4.4"],
ipv6: ["2001:4860:4860::8888", "2001:4860:4860::8844"],
translationKey: "network_issue.use_google",
},
];
export async function getSupervisorLogs(lines = 100) {
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
});
}
export async function getSupervisorLogsFollow(lines = 500) {
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
headers: {
Accept: "text/plain",
},
});
}
export async function getSupervisorNetworkInfo() {
return fetch("/supervisor/network/info");
}
export const setSupervisorNetworkDns = async (
dnsServerIndex: number,
primaryInterface: string
) =>
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
method: "POST",
body: JSON.stringify({
ipv4: {
method: "auto",
nameservers: ALTERNATIVE_DNS_SERVERS[dnsServerIndex].ipv4,
},
ipv6: {
method: "auto",
nameservers: ALTERNATIVE_DNS_SERVERS[dnsServerIndex].ipv6,
},
}),
headers: {
"Content-Type": "application/json",
},
});

View File

@@ -1,3 +0,0 @@
import "./ha-landing-page";
import("../../src/resources/ha-style");

View File

@@ -1,182 +0,0 @@
import "@material/mwc-linear-progress";
import { type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../src/components/ha-alert";
import { haStyle } from "../../src/resources/styles";
import "../../src/onboarding/onboarding-welcome-links";
import "./components/landing-page-network";
import "./components/landing-page-logs";
import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
const SCHEDULE_CORE_CHECK_SECONDS = 5;
@customElement("ha-landing-page")
class HaLandingPage extends LandingPageBaseElement {
@property({ attribute: false }) public translationFragment = "landing-page";
@state() private _networkIssue = false;
@state() private _supervisorError = false;
private _mobileApp =
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
render() {
return html`
<ha-card>
<div class="card-content">
<h1>${this.localize("header")}</h1>
${!this._networkIssue && !this._supervisorError
? html`
<p>${this.localize("subheader")}</p>
<mwc-linear-progress indeterminate></mwc-linear-progress>
`
: nothing}
<landing-page-network
@value-changed=${this._networkInfoChanged}
.localize=${this.localize}
></landing-page-network>
${this._supervisorError
? html`
<ha-alert
alert-type="error"
.title=${this.localize("error_title")}
>
${this.localize("error_description")}
</ha-alert>
`
: nothing}
<landing-page-logs
.localize=${this.localize}
@landing-page-error=${this._showError}
></landing-page-logs>
</div>
</ha-card>
<onboarding-welcome-links
.localize=${this.localize}
.mobileApp=${this._mobileApp}
></onboarding-welcome-links>
<div class="footer">
<ha-language-picker
.value=${this.language}
.label=${""}
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/getting-started/onboarding/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-onboarding.help")}</a
>
</div>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
makeDialogManager(this, this.shadowRoot!);
if (window.innerWidth > 450) {
import("../../src/resources/particles");
}
import("../../src/components/ha-language-picker");
this._scheduleCoreCheck();
}
private _scheduleCoreCheck() {
setTimeout(
() => this._checkCoreAvailability(),
SCHEDULE_CORE_CHECK_SECONDS * 1000
);
}
private async _checkCoreAvailability() {
try {
const response = await fetch("/manifest.json");
if (response.ok) {
location.reload();
}
} finally {
this._scheduleCoreCheck();
}
}
private _showError() {
this._supervisorError = true;
}
private _networkInfoChanged(ev: CustomEvent) {
this._networkIssue = ev.detail.value;
}
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
if (language !== this.language && language) {
this.language = language;
try {
window.localStorage.setItem(
"selectedLanguage",
JSON.stringify(language)
);
} catch (err: any) {
// Ignore
}
}
}
static styles = [
haStyle,
onBoardingStyles,
css`
.footer {
padding-top: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
ha-card .card-content {
display: flex;
flex-direction: column;
gap: 16px;
}
ha-alert p {
text-align: unset;
}
ha-language-picker {
display: block;
width: 200px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-landing-page": HaLandingPage;
}
}

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home Assistant</title>
<%= renderTemplate("../../../src/html/_header.html.template") %>
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
<style>
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
body {
height: auto;
padding: 32px 0;
}
.content {
max-width: 560px;
margin: 0 auto;
padding: 0 16px;
box-sizing: content-box;
}
.header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32px;
}
.header img {
height: 56px;
width: 56px;
}
</style>
</head>
<body id="particles">
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" alt="Home Assistant" />
</div>
<ha-landing-page></ha-landing-page>
</div>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
</body>
</html>

View File

@@ -1,74 +0,0 @@
import type { PropertyValues } from "lit";
import { LitElement } from "lit";
import { property, state } from "lit/decorators";
import {
computeLocalize,
type LandingPageKeys,
type LocalizeFunc,
} from "../../src/common/translations/localize";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import type { Constructor, Resources } from "../../src/types";
import {
getLocalLanguage,
getTranslation,
} from "../../src/util/common-translation";
import { computeDirectionStyles } from "../../src/common/util/compute_rtl";
import themesMixin from "../../src/state/themes-mixin";
import { translationMetadata } from "../../src/resources/translations-metadata";
import type { HassBaseEl } from "../../src/state/hass-base-mixin";
export class LandingPageBaseElement extends themesMixin(
ProvideHassLitMixin(LitElement) as unknown as Constructor<HassBaseEl>
) {
// Initialized to empty will prevent undefined errors if called before connected to DOM.
@property({ attribute: false })
public localize: LocalizeFunc<LandingPageKeys> = () => "";
// Use browser language setup before login.
@property() public language?: string = getLocalLanguage();
@state() private _resources?: Resources;
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.get("language")) {
this._resources = undefined;
this._initializeLocalize();
}
if (
this.language &&
this._resources &&
(changedProperties.has("language") || changedProperties.has("_resources"))
) {
this._setLocalize();
}
}
private async _initializeLocalize() {
if (this._resources || !this.language) {
return;
}
const { data } = await getTranslation(null, this.language);
this._resources = {
[this.language]: data,
};
}
private async _setLocalize() {
this.localize = await computeLocalize(
this.constructor.prototype,
this.language!,
this._resources!
);
computeDirectionStyles(
translationMetadata.translations[this.language!].isRTL,
this
);
}
}

View File

@@ -8,7 +8,7 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore",
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write",
@@ -19,8 +19,7 @@
"postinstall": "husky",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"test": "vitest run --config test/vitest.config.ts",
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\""
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
@@ -30,21 +29,20 @@
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.3",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.6",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/search": "6.5.7",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.35.2",
"@codemirror/view": "6.34.3",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.6",
"@formatjs/intl-displaynames": "6.8.6",
"@formatjs/intl-durationformat": "0.6.5",
"@formatjs/intl-getcanonicallocales": "2.5.3",
"@formatjs/intl-listformat": "7.7.6",
"@formatjs/intl-locale": "4.2.6",
"@formatjs/intl-numberformat": "8.14.6",
"@formatjs/intl-pluralrules": "5.3.6",
"@formatjs/intl-relativetimeformat": "11.4.6",
"@formatjs/intl-datetimeformat": "6.16.4",
"@formatjs/intl-displaynames": "6.8.4",
"@formatjs/intl-getcanonicallocales": "2.5.2",
"@formatjs/intl-listformat": "7.7.4",
"@formatjs/intl-locale": "4.2.4",
"@formatjs/intl-numberformat": "8.14.4",
"@formatjs/intl-pluralrules": "5.3.4",
"@formatjs/intl-relativetimeformat": "11.4.4",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -54,7 +52,7 @@
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.4",
"@lit-labs/observers": "2.0.2",
"@lit-labs/virtualizer": "2.0.14",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
@@ -91,8 +89,8 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.5",
"@vaadin/vaadin-themable-mixin": "24.5.5",
"@vaadin/combo-box": "24.5.3",
"@vaadin/vaadin-themable-mixin": "24.5.3",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -101,7 +99,7 @@
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"chart.js": "4.4.7",
"chart.js": "4.4.6",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.39.0",
@@ -111,20 +109,19 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.12",
"element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.8",
"intl-messageformat": "10.7.6",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.3",
"marked": "15.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -162,25 +159,24 @@
"@babel/preset-env": "7.26.0",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.17.0",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.8.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
"@rsdoctor/rspack-plugin": "0.4.11",
"@rspack/cli": "1.1.5",
"@rspack/core": "1.1.5",
"@open-wc/dev-server-hmr": "0.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.20",
"@types/chromecast-caf-sender": "1.0.11",
"@types/chromecast-caf-receiver": "6.0.18",
"@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.15",
"@types/leaflet": "1.9.14",
"@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
"@types/mocha": "10.0.9",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8",
@@ -189,12 +185,13 @@
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitest/coverage-v8": "2.1.8",
"@web/dev-server": "0.1.38",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.2",
"del": "8.0.0",
"eslint": "9.16.0",
"eslint": "9.15.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
@@ -211,29 +208,34 @@
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "25.0.1",
"husky": "9.1.6",
"instant-mocha": "1.5.3",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.14",
"magic-string": "0.30.12",
"map-stream": "0.0.7",
"mocha": "10.8.2",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.4.2",
"rspack-manifest-plugin": "5.0.2",
"prettier": "3.3.3",
"serve-handler": "6.1.6",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.2",
"vitest": "2.1.8",
"typescript": "5.6.3",
"webpack": "5.96.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -243,11 +245,10 @@
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0",
"lit-html": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.13.0"
"globals": "15.12.0"
},
"packageManager": "yarn@4.5.3"
"packageManager": "yarn@4.5.1"
}

View File

@@ -1,3 +0,0 @@
<svg width="92" height="136" viewBox="0 0 92 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.38 104.9H3.03C2.24231 104.9 1.48688 105.213 0.929897 105.77C0.372914 106.327 0.0600052 107.082 0.0600052 107.87V132.47C0.0600052 133.258 0.372914 134.013 0.929897 134.57C1.48688 135.127 2.24231 135.44 3.03 135.44H12.36C13.1477 135.44 13.9031 135.127 14.4601 134.57C15.0171 134.013 15.33 133.258 15.33 132.47V120.17H76.39V132.47C76.39 133.258 76.7029 134.013 77.2599 134.57C77.8169 135.127 78.5723 135.44 79.36 135.44H88.69C89.4777 135.44 90.2331 135.127 90.7901 134.57C91.3471 134.013 91.66 133.258 91.66 132.47V107.87C91.66 107.082 91.3471 106.327 90.7901 105.77C90.2331 105.213 89.4777 104.9 88.69 104.9H76.39H76.38ZM50.04 2.24996C47.73 -0.0600439 43.95 -0.0600439 41.65 2.24996L4.25 39.65C1.94 41.96 0.0500031 46.52 0.0500031 49.78V83.7C0.0500031 86.96 2.72 89.64 5.99 89.64H85.7C88.96 89.64 91.64 86.97 91.64 83.7V49.78C91.64 46.52 89.75 41.96 87.44 39.65L50.04 2.24996Z" fill="#18BCF2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1011 B

View File

@@ -1,10 +0,0 @@
<svg width="75" height="79" viewBox="0 0 75 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" fill="url(#paint0_linear_549_34)"/>
<path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_549_34" x1="37.0692" y1="0" x2="37.0692" y2="79" gradientUnits="userSpaceOnUse">
<stop stop-color="#6364FF"/>
<stop offset="1" stop-color="#563ACC"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241127.0"
version = "20241106.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@@ -43,6 +43,12 @@
"description": "Group date-fns with dependent timezone package",
"groupName": "date-fns",
"matchPackageNames": ["date-fns", "date-fns-tz"]
},
{
"description": "Group and temporarily disable WDS packages",
"groupName": "Web Dev Server",
"enabled": false,
"matchPackageNames": ["@web/dev-server{/,}**"]
}
]
}

View File

@@ -1,70 +0,0 @@
#!/bin/sh
#
# This script can be used to develop and test the frontend without having to
# link the build in a running core instance through the frontend/development_repo setting.
#
# WARNING:
# If you have an active login session in the frontend. The core that was used
# as a backend during the time of the login remains used until you logout again.
# So if you reuse the url hosting the frontend, you will need to logout before
# it will actually start using the core backend configured by this script.
#
# If you run this script without parameters, the frontend will be accessible under http://localhost:8124.
# And it will use the core instance running under http://localhost:8123 as a backend.
# Note that from a devcontainer, the frontend will be accessible under port 8124 on the host container.
# Inside the devcontainer it will be accessible under port 8123 instead.
# The core instance endpoint remains the same in both cases, as this is resolved from the browser.
#
# You can change the core instance the frontend connects to by passing the -c option.
# For example: script/develop_and_serve -c https://myhost.duckdns.org:8123
# This will also work for existing production core instances.
# It does not need to be a development version hosted locally.
#
# You can change the port the frontend is served on by passing the -p option.
# For example: script/develop_and_serve -p 8654
# Note that if you are running from a devcontainer, you will need to setup
# port forwarding as well if you want to access it from the container host.
# Stop on errors
set -e
cd "$(dirname "$0")/.."
# parse input paramters
if [ -n "$DEVCONTAINER" ]; then
frontendPort=8123
else
frontendPort=8124
fi
coreUrl=http://localhost:8123
while getopts p:c:h flag
do
case "${flag}" in
p) frontendPort=${OPTARG};;
c) coreUrl="${OPTARG}";;
h) echo Documentation can be found inside "$0" && exit 0;;
*) echo Documentation can be found inside "$0" && exit 1;;
esac
done
# display used settings
if [ -n "$DEVCONTAINER" ]; then
echo Frontend is available inside container as http://localhost:${frontendPort}
if [ 8123 -eq $frontendPort ]; then
echo Frontend is available on container host as http://localhost:8124
fi
else
echo Frontend is hosted on http://localhost:${frontendPort}
fi
echo Core is used from ${coreUrl}
# build the frontend so it connects to the passed core
HASS_URL="$coreUrl" ./script/develop &
# serve the frontend
yarn dlx serve -l $frontendPort ./hass_frontend -s &
# keep the script running while serving
wait

View File

@@ -6,4 +6,9 @@ set -e
cd "$(dirname "$0")/.."
./node_modules/.bin/gulp analyze-app
export STATS=1
statsfile="compilation-stats.json"
./node_modules/.bin/webpack-cli --profile --node-env=production --json=$statsfile
npx webpack-bundle-analyzer $statsfile hass_frontend/frontend_latest
rm -f $statsfile

View File

@@ -30,17 +30,17 @@ type State = "loading" | "error" | "step";
export class HaAuthFlow extends LitElement {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property({ attribute: false }) public clientId?: string;
@property() public clientId?: string;
@property({ attribute: false }) public redirectUri?: string;
@property() public redirectUri?: string;
@property({ attribute: false }) public oauth2State?: string;
@property() public oauth2State?: string;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ attribute: false }) public initStoreToken = false;
@property({ type: Boolean }) public initStoreToken = false;
@state() private _storeToken = false;

View File

@@ -21,13 +21,13 @@ const appNames = {
@customElement("ha-authorize")
export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property({ attribute: false }) public clientId?: string;
@property() public clientId?: string;
@property({ attribute: false }) public redirectUri?: string;
@property() public redirectUri?: string;
@property({ attribute: false }) public oauth2State?: string;
@property() public oauth2State?: string;
@property({ attribute: false }) public translationFragment = "page-authorize";
@property() public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@@ -202,9 +202,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
<ha-language-picker
.value=${this.language}
.label=${""}
native-name
nativeName
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/docs/authentication/"
@@ -327,7 +326,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this.language = language;
try {
window.localStorage.setItem("selectedLanguage", JSON.stringify(language));
localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}

View File

@@ -1,8 +1,3 @@
/**
* Get all possible combinations of an array
* @param arr - The array to get combinations of
* @returns A multidimensional array of all possible combinations
*/
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>

View File

@@ -1,9 +1,5 @@
type NonUndefined<T> = T extends undefined ? never : T;
/**
* Ensure that the input is an array or wrap it in an array
* @param value - The value to ensure is an array
*/
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];

View File

@@ -1,8 +1,4 @@
/**
* Creates a type predicate function for determining if an array literal includes a given value
* @param array - The array to check
* @returns A type predicate function
*/
// Creates a type predicate function for determining if an array literal includes a given value
export const arrayLiteralIncludes =
<T extends readonly unknown[]>(array: T) =>
(searchElement: unknown, fromIndex?: number): searchElement is T[number] =>

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