mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-28 08:56:19 +00:00
Merge branch 'dev' into fix-53320
This commit is contained in:
@@ -44,3 +44,13 @@ http://localhost:8090/?demo=<second slug>#/energy/water
|
||||
- `src/configs/demo-configs.ts`: Registry of demo configurations and URL slug handling.
|
||||
- `src/stubs/`: Mocked WebSocket/REST APIs.
|
||||
- `script/develop_demo`, `script/build_demo`: Dev server and static build wrappers.
|
||||
|
||||
## The gallery imports these stubs too
|
||||
|
||||
`demo/src/stubs/` is shared, not demo-private: gallery pages import from it directly. Before changing or removing anything a stub does, grep for its callers across `gallery/` as well as `demo/`, and check the affected gallery pages, not just the demo.
|
||||
|
||||
```bash
|
||||
grep -rn "stubs/<name>" demo/src gallery/src
|
||||
```
|
||||
|
||||
The two consume a stub differently, so demo behavior does not predict gallery behavior. A gallery page calls stubs against the `hass` from `provideHass`, where `hass.config` is the shared `demoConfig` object, so a stub that mutates `hass.config` in place is visible to the page. `ha-demo.ts` copies `components` into a new array before the stubs run, so the same mutation never reaches the demo. A change can therefore look fine in the demo while quietly breaking a gallery page.
|
||||
|
||||
+6
-4
@@ -16,6 +16,8 @@ import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry, setDemoFloors } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockHardware } from "./stubs/hardware";
|
||||
import { mockHassioSupervisor } from "./stubs/hassio_supervisor";
|
||||
import { mockIntegration } from "./stubs/integration";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
@@ -73,10 +75,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
// `contextMixin`, so let provideHass skip them to avoid duplicate providers.
|
||||
const hass = provideHass(this, initial, true, false);
|
||||
|
||||
// The cloud account page only fetches backup config and the webhook count
|
||||
// when those integrations are loaded. Enable them here (demo only) so the
|
||||
// mocked backup/config/info and webhook/list are queried. usage_prediction
|
||||
// is needed for common-controls sections in strategy dashboards.
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
@@ -86,6 +84,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
"webhook",
|
||||
"usage_prediction",
|
||||
"assist_pipeline",
|
||||
"hassio",
|
||||
"hardware",
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -113,6 +113,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEvents(hass);
|
||||
mockMediaPlayer(hass);
|
||||
mockFrontend(hass);
|
||||
mockHardware(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
mockIcons(hass);
|
||||
mockEnergy(hass);
|
||||
mockPersistentNotification(hass);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type {
|
||||
HardwareInfo,
|
||||
SystemStatusStreamMessage,
|
||||
} from "../../../src/data/hardware";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// Mirrors what homeassistant_green reports, so the hardware page resolves the
|
||||
// board name and the brands image the same way it does on a real Green.
|
||||
const HARDWARE_INFO: HardwareInfo = {
|
||||
hardware: [
|
||||
{
|
||||
board: {
|
||||
hassio_board_id: "green",
|
||||
manufacturer: "homeassistant",
|
||||
model: "green",
|
||||
},
|
||||
dongle: null,
|
||||
config_entries: [],
|
||||
name: "Home Assistant Green",
|
||||
url: "https://support.nabucasa.com/hc/en-us/categories/24638797677853-Home-Assistant-Green",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockHardware = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("hardware/info", () => HARDWARE_INFO);
|
||||
|
||||
hass.mockWS(
|
||||
"hardware/subscribe_system_status",
|
||||
(_msg, _currentHass, onChange) => {
|
||||
// Rounded like the hardware integration rounds psutil's values.
|
||||
const send = () => {
|
||||
const usedMb = 1560 + Math.round(Math.random() * 80);
|
||||
const message: SystemStatusStreamMessage = {
|
||||
cpu_percent: Math.round((8 + Math.random() * 6) * 10) / 10,
|
||||
memory_free_mb: 4096 - usedMb,
|
||||
memory_used_mb: usedMb,
|
||||
memory_used_percent: Math.round((usedMb / 4096) * 1000) / 10,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
onChange?.(message);
|
||||
};
|
||||
send();
|
||||
const interval = window.setInterval(send, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,59 +1,385 @@
|
||||
import type { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
||||
import type {
|
||||
HassioAddonDetails,
|
||||
HassioAddonInfo,
|
||||
HassioAddonsInfo,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import type { HassioStats } from "../../../src/data/hassio/common";
|
||||
import type {
|
||||
HassioHassOSInfo,
|
||||
HassioHostInfo,
|
||||
HostDisksUsage,
|
||||
} from "../../../src/data/hassio/host";
|
||||
import type { NetworkInfo } from "../../../src/data/hassio/network";
|
||||
import type {
|
||||
HassioInfo,
|
||||
HassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import type { SupervisorMounts } from "../../../src/data/supervisor/mounts";
|
||||
import type { SupervisorUpdateConfig } from "../../../src/data/supervisor/update";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// `icon`/`logo` are false on purpose: the panel would otherwise request
|
||||
// /api/hassio/addons/<slug>/icon, which the demo has no backend for.
|
||||
const DEMO_ADDONS: HassioAddonInfo[] = [
|
||||
{
|
||||
name: "Music Assistant",
|
||||
slug: "d5369777_music_assistant",
|
||||
description:
|
||||
"Music library manager for all your media sources and streaming services, with support for a wide range of players",
|
||||
advanced: false,
|
||||
available: true,
|
||||
build: false,
|
||||
detached: false,
|
||||
homeassistant: "2025.7.0",
|
||||
icon: false,
|
||||
installed: true,
|
||||
logo: false,
|
||||
repository: "d5369777",
|
||||
stage: "stable",
|
||||
state: "started",
|
||||
update_available: false,
|
||||
url: "https://github.com/music-assistant/home-assistant-addon",
|
||||
version: "2.6.3",
|
||||
version_latest: "2.6.3",
|
||||
},
|
||||
{
|
||||
name: "ESPHome Device Builder",
|
||||
slug: "5c53de3b_esphome",
|
||||
description:
|
||||
"Manage and program your ESP8266/ESP32 based microcontrollers directly via WiFi and with a simple, yet powerful configuration file syntax",
|
||||
advanced: false,
|
||||
available: true,
|
||||
build: false,
|
||||
detached: false,
|
||||
homeassistant: "2025.7.0",
|
||||
icon: false,
|
||||
installed: true,
|
||||
logo: false,
|
||||
repository: "5c53de3b",
|
||||
stage: "stable",
|
||||
state: "started",
|
||||
update_available: false,
|
||||
url: "https://esphome.io/",
|
||||
version: "2025.7.3",
|
||||
version_latest: "2025.7.3",
|
||||
},
|
||||
];
|
||||
|
||||
const LONG_DESCRIPTIONS: Record<string, string> = {
|
||||
d5369777_music_assistant: `## Music Assistant
|
||||
|
||||
Music Assistant brings all your music sources together in one library and streams
|
||||
them to the players you already own.
|
||||
|
||||
- Combines local files with streaming services into a single searchable library
|
||||
- Plays to Sonos, Chromecast, AirPlay, Squeezebox, DLNA and Home Assistant media players
|
||||
- Group players together for synced multi-room audio
|
||||
- Exposes players and playlists to Home Assistant automations and voice assistants`,
|
||||
"5c53de3b_esphome": `## ESPHome Device Builder
|
||||
|
||||
ESPHome turns an ESP8266 or ESP32 into a Home Assistant device using a short YAML
|
||||
configuration instead of hand-written firmware.
|
||||
|
||||
- Compile and flash firmware straight from the browser, over WiFi after the first flash
|
||||
- Hundreds of supported sensors, displays, lights and switches
|
||||
- Devices are discovered by Home Assistant automatically, with no cloud in between
|
||||
- Configuration lives next to your Home Assistant config, so it is covered by backups`,
|
||||
};
|
||||
|
||||
// Supervisor schema format (converted to selectors by the config tab). Music
|
||||
// Assistant is configured in its own UI, so it has no add-on options.
|
||||
const CONFIG_SCHEMAS: Record<string, HassioAddonDetails["schema"]> = {
|
||||
"5c53de3b_esphome": [
|
||||
{ name: "ssl", type: "boolean", required: true },
|
||||
{ name: "certfile", type: "string", required: true },
|
||||
{ name: "keyfile", type: "string", required: true },
|
||||
{ name: "leave_front_door_open", type: "boolean", required: false },
|
||||
{ name: "status_use_ping", type: "boolean", required: false },
|
||||
],
|
||||
};
|
||||
|
||||
const CONFIG_OPTIONS: Record<string, Record<string, unknown>> = {
|
||||
"5c53de3b_esphome": {
|
||||
ssl: false,
|
||||
certfile: "fullchain.pem",
|
||||
keyfile: "privkey.pem",
|
||||
},
|
||||
};
|
||||
|
||||
const addonDetails = (addon: HassioAddonInfo): HassioAddonDetails => ({
|
||||
...addon,
|
||||
apparmor: "default",
|
||||
arch: ["aarch64", "amd64"],
|
||||
audio_input: null,
|
||||
audio_output: null,
|
||||
audio: false,
|
||||
auth_api: false,
|
||||
auto_uart: false,
|
||||
auto_update: false,
|
||||
boot: "auto",
|
||||
changelog: false,
|
||||
devices: [],
|
||||
devicetree: false,
|
||||
discovery: [],
|
||||
docker_api: false,
|
||||
documentation: false,
|
||||
full_access: false,
|
||||
gpio: false,
|
||||
hassio_api: false,
|
||||
hassio_role: "default",
|
||||
hostname: addon.slug.replace(/_/g, "-"),
|
||||
homeassistant_api: false,
|
||||
host_dbus: false,
|
||||
host_ipc: false,
|
||||
host_network: false,
|
||||
host_pid: false,
|
||||
ingress_entry: null,
|
||||
ingress_panel: false,
|
||||
ingress_url: null,
|
||||
ingress: false,
|
||||
ip_address: "172.30.33.2",
|
||||
kernel_modules: false,
|
||||
long_description: LONG_DESCRIPTIONS[addon.slug],
|
||||
machine: [],
|
||||
network_description: null,
|
||||
network: null,
|
||||
options: CONFIG_OPTIONS[addon.slug] ?? {},
|
||||
privileged: [],
|
||||
protected: true,
|
||||
rating: 6,
|
||||
schema: CONFIG_SCHEMAS[addon.slug] ?? null,
|
||||
services_role: [],
|
||||
signed: false,
|
||||
startup: "application",
|
||||
stdin: false,
|
||||
system_managed: false,
|
||||
system_managed_config_entry: null,
|
||||
translations: {},
|
||||
watchdog: true,
|
||||
webui: null,
|
||||
});
|
||||
|
||||
const LOGS: Record<string, string> = {
|
||||
d5369777_music_assistant: `[server] Starting Music Assistant Server 2.6.3
|
||||
[server] Loaded provider: filesystem_local
|
||||
[server] Loaded provider: spotify
|
||||
[server] Loaded provider: sonos
|
||||
[players] Discovered player: Living Room (Sonos)
|
||||
[players] Discovered player: Kitchen (Chromecast)
|
||||
[server] Music Assistant is ready
|
||||
`,
|
||||
"5c53de3b_esphome": `[esphome] Starting ESPHome Device Builder 2025.7.3
|
||||
[esphome] Dashboard running on port 6052
|
||||
[esphome] Found 3 configurations
|
||||
[esphome] bedroom-sensor is online (2025.7.3)
|
||||
[esphome] garage-door is online (2025.7.3)
|
||||
[esphome] office-display is online (2025.7.3)
|
||||
`,
|
||||
};
|
||||
|
||||
const ADDON_STATS: HassioStats = {
|
||||
blk_read: 12300000,
|
||||
blk_write: 4500000,
|
||||
cpu_percent: 1.4,
|
||||
memory_limit: 3900000000,
|
||||
memory_percent: 4.2,
|
||||
memory_usage: 163000000,
|
||||
network_rx: 8900000,
|
||||
network_tx: 2300000,
|
||||
};
|
||||
|
||||
export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
|
||||
hass.config.components.push("hassio");
|
||||
// Gallery pages rely on this to enable the hassio-gated pickers. The demo
|
||||
// lists hassio in its own components, hence the guard against duplicates.
|
||||
if (!hass.config.components.includes("hassio")) {
|
||||
hass.config.components.push("hassio");
|
||||
}
|
||||
|
||||
hass.mockWS("supervisor/api", (msg) => {
|
||||
if (msg.endpoint === "/supervisor/info") {
|
||||
const data: HassioSupervisorInfo = {
|
||||
version: "2021.10.dev0805",
|
||||
version_latest: "2021.10.dev0806",
|
||||
update_available: true,
|
||||
channel: "dev",
|
||||
version: "2026.07.1",
|
||||
version_latest: "2026.07.1",
|
||||
update_available: false,
|
||||
channel: "stable",
|
||||
arch: "aarch64",
|
||||
supported: true,
|
||||
healthy: true,
|
||||
ip_address: "172.30.32.2",
|
||||
wait_boot: 5,
|
||||
timezone: "America/Los_Angeles",
|
||||
timezone: "Europe/Amsterdam",
|
||||
logging: "info",
|
||||
debug: false,
|
||||
debug_block: false,
|
||||
diagnostics: true,
|
||||
addons: [
|
||||
{
|
||||
name: "Visual Studio Code",
|
||||
slug: "a0d7b954_vscode",
|
||||
description:
|
||||
"Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!",
|
||||
state: "started",
|
||||
version: "3.6.2",
|
||||
version_latest: "3.6.2",
|
||||
update_available: false,
|
||||
repository: "a0d7b954",
|
||||
icon: false,
|
||||
logo: true,
|
||||
},
|
||||
{
|
||||
name: "Z-Wave JS",
|
||||
slug: "core_zwave_js",
|
||||
description:
|
||||
"Control a ZWave network with Home Assistant Z-Wave JS",
|
||||
state: "started",
|
||||
version: "0.1.45",
|
||||
version_latest: "0.1.45",
|
||||
update_available: false,
|
||||
repository: "core",
|
||||
icon: true,
|
||||
logo: true,
|
||||
},
|
||||
] as any,
|
||||
addons: DEMO_ADDONS as any,
|
||||
addons_repositories: [
|
||||
"https://github.com/hassio-addons/repository",
|
||||
"https://github.com/music-assistant/home-assistant-addon",
|
||||
"https://github.com/esphome/home-assistant-addon",
|
||||
] as any,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/addons") {
|
||||
const data: HassioAddonsInfo = {
|
||||
addons: DEMO_ADDONS,
|
||||
repositories: [
|
||||
{
|
||||
slug: "d5369777",
|
||||
name: "Music Assistant",
|
||||
source: "https://github.com/music-assistant/home-assistant-addon",
|
||||
url: "https://github.com/music-assistant/home-assistant-addon",
|
||||
maintainer: "Music Assistant",
|
||||
},
|
||||
{
|
||||
slug: "5c53de3b",
|
||||
name: "ESPHome",
|
||||
source: "https://github.com/esphome/home-assistant-addon",
|
||||
url: "https://esphome.io/",
|
||||
maintainer: "ESPHome",
|
||||
},
|
||||
],
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const addonMatch = msg.endpoint.match(/^\/addons\/([^/]+)\/(info|stats)$/);
|
||||
if (addonMatch) {
|
||||
const addon = DEMO_ADDONS.find((item) => item.slug === addonMatch[1]);
|
||||
if (!addon) {
|
||||
return Promise.reject(`Addon ${addonMatch[1]} not found`);
|
||||
}
|
||||
return addonMatch[2] === "stats" ? ADDON_STATS : addonDetails(addon);
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/info") {
|
||||
const data: HassioInfo = {
|
||||
arch: "aarch64",
|
||||
channel: "stable",
|
||||
docker: "27.5.1",
|
||||
features: ["reboot", "shutdown", "network", "hostname", "os_agent"],
|
||||
hassos: null,
|
||||
homeassistant: "2026.7.2",
|
||||
hostname: "homeassistant",
|
||||
logging: "info",
|
||||
machine: "green",
|
||||
state: "running",
|
||||
operating_system: "Home Assistant OS 18.2",
|
||||
supervisor: "2026.07.1",
|
||||
supported: true,
|
||||
supported_arch: ["aarch64", "armv7", "armhf"],
|
||||
timezone: "Europe/Amsterdam",
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/host/info") {
|
||||
const data: HassioHostInfo = {
|
||||
agent_version: "1.8.0",
|
||||
chassis: "embedded",
|
||||
cpe: "cpe:2.3:o:home-assistant:haos:18.2:*:production:*:*:*:aarch64:*",
|
||||
deployment: "production",
|
||||
disk_life_time: 6,
|
||||
disk_free: 22.3,
|
||||
disk_total: 31.2,
|
||||
disk_used: 8.9,
|
||||
features: ["reboot", "shutdown", "network", "hostname", "os_agent"],
|
||||
hostname: "homeassistant",
|
||||
kernel: "6.12.48-haos",
|
||||
operating_system: "Home Assistant OS 18.2",
|
||||
boot_timestamp: 1751932800000000,
|
||||
startup_time: 12.4,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/os/info") {
|
||||
const data: HassioHassOSInfo = {
|
||||
board: "green",
|
||||
boot: "A",
|
||||
update_available: false,
|
||||
version: "18.2",
|
||||
version_latest: "18.2",
|
||||
data_disk: "Home Assistant Green (mmcblk0)",
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/host/disks/default/usage") {
|
||||
const data: HostDisksUsage = {
|
||||
id: "root",
|
||||
label: "Total",
|
||||
total_bytes: 31200000000,
|
||||
used_bytes: 8900000000,
|
||||
children: [
|
||||
{ id: "media", label: "Media", used_bytes: 4100000000 },
|
||||
{ id: "addons", label: "Apps", used_bytes: 2600000000 },
|
||||
{ id: "backup", label: "Backups", used_bytes: 1400000000 },
|
||||
{ id: "share", label: "Share", used_bytes: 800000000 },
|
||||
],
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/mounts") {
|
||||
const data: SupervisorMounts = {
|
||||
default_backup_mount: null,
|
||||
mounts: [],
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/network/info") {
|
||||
const data: NetworkInfo = {
|
||||
interfaces: [
|
||||
{
|
||||
primary: true,
|
||||
privacy: false,
|
||||
interface: "eth0",
|
||||
enabled: true,
|
||||
type: "ethernet",
|
||||
ipv4: {
|
||||
address: ["192.168.1.10/24"],
|
||||
gateway: "192.168.1.1",
|
||||
method: "auto",
|
||||
nameservers: ["192.168.1.1"],
|
||||
},
|
||||
wifi: null,
|
||||
},
|
||||
],
|
||||
docker: {
|
||||
address: "172.30.32.0/23",
|
||||
dns: "172.30.32.3",
|
||||
gateway: "172.30.32.1",
|
||||
interface: "hassio",
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
if (msg.endpoint === "/store/reload") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`);
|
||||
});
|
||||
|
||||
hass.mockWS("hassio/update/config/info", (): SupervisorUpdateConfig => ({
|
||||
add_on_backup_before_update: true,
|
||||
add_on_backup_retain_copies: 1,
|
||||
core_backup_before_update: true,
|
||||
}));
|
||||
|
||||
hass.mockAPI(/^hassio\/host\/logs\/boots$/, () => ({
|
||||
data: { boots: { "0": "2026-07-26T09:00:00.000000+00:00" } },
|
||||
}));
|
||||
|
||||
hass.mockAPI(/^hassio\/addons\/[^/]+\/logs/, (_hass, _method, path) => {
|
||||
const slug = path.split("/")[2];
|
||||
// X-First-Cursor tells error-log-card there is nothing older to page to.
|
||||
return new Response(LOGS[slug], {
|
||||
headers: { "X-First-Cursor": "demo" },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
.dot-top { animation: pulse-top 1300ms 350ms linear infinite; }
|
||||
@keyframes pulse-left {
|
||||
0% { transform: translate(50px, 69.634804px) scale(1); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
15.384615% { transform: translate(50px, 69.634804px) scale(1.2); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
15.384615% { transform: translate(50px, 69.634804px) scale(1.3); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
38.461538%, 100% { transform: translate(50px, 69.634804px) scale(1); }
|
||||
}
|
||||
@keyframes pulse-right {
|
||||
0%, 15.384615% { transform: translate(90px, 58.634798px) scale(1); }
|
||||
15.384615% { animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
30.769231% { transform: translate(90px, 58.634798px) scale(1.2); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
30.769231% { transform: translate(90px, 58.634798px) scale(1.3); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
53.846154%, 100% { transform: translate(90px, 58.634798px) scale(1); }
|
||||
}
|
||||
@keyframes pulse-top {
|
||||
0%, 30.769231% { transform: translate(70px, 37.6348px) scale(1); }
|
||||
30.769231% { animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
46.153846% { transform: translate(70px, 37.6348px) scale(1.2); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
46.153846% { transform: translate(70px, 37.6348px) scale(1.3); animation-timing-function: cubic-bezier(.39, .575, .565, 1); }
|
||||
69.230769%, 100% { transform: translate(70px, 37.6348px) scale(1); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -95,7 +95,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
options: Record<string, unknown>;
|
||||
privileged: any;
|
||||
protected: boolean;
|
||||
rating: "1-8";
|
||||
rating: number;
|
||||
schema: HaFormSchema[] | null;
|
||||
services_role: string[];
|
||||
signed: boolean;
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface StoreAddonDetails extends StoreAddon {
|
||||
apparmor: boolean;
|
||||
arch: SupervisorArch[];
|
||||
auth_api: boolean;
|
||||
changelog: boolean;
|
||||
detached: boolean;
|
||||
docker_api: boolean;
|
||||
documentation: boolean;
|
||||
|
||||
@@ -481,6 +481,17 @@ export const provideHass = (
|
||||
? response[1](hass(), method, path, parameters)
|
||||
: Promise.reject(`API Mock for ${path} is not implemented`);
|
||||
},
|
||||
// Mocks return a plain body; wrap it so callers can stream it like a fetch
|
||||
// Response. Callbacks may return a Response themselves to set headers.
|
||||
async callApiRaw(method, path, parameters, headers) {
|
||||
const result = await hassObj.callApi<any>(
|
||||
method,
|
||||
path,
|
||||
parameters,
|
||||
headers
|
||||
);
|
||||
return result instanceof Response ? result : new Response(result);
|
||||
},
|
||||
hassUrl: (path?) => path,
|
||||
fetchWithAuth: () => Promise.reject("Not implemented"),
|
||||
sendWS: (msg) => hassObj.connection.sendMessage(msg),
|
||||
|
||||
@@ -132,11 +132,12 @@
|
||||
}
|
||||
}
|
||||
@media (max-height: 560px) {
|
||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
/* body selector to avoid minification causing bad jinja2 */
|
||||
body #ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
body #ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
padding-top: 16px;
|
||||
}
|
||||
.ohf-logo {
|
||||
|
||||
@@ -225,20 +225,33 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
"ui.panel.config.apps.dashboard.current_version",
|
||||
{ version: this._currentAddon.version }
|
||||
)}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link"
|
||||
>${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>
|
||||
${
|
||||
this._currentAddon.changelog
|
||||
? html`<div
|
||||
class="changelog"
|
||||
@click=${this._openChangelog}
|
||||
>
|
||||
(<span class="changelog-link"
|
||||
>${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: html`${this._currentAddon.version_latest}
|
||||
<span class="changelog-link" @click=${this._openChangelog}
|
||||
>${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
)}</span
|
||||
>`
|
||||
${
|
||||
this._currentAddon.changelog
|
||||
? html`<span
|
||||
class="changelog-link"
|
||||
@click=${this._openChangelog}
|
||||
>${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.changelog"
|
||||
)}</span
|
||||
>`
|
||||
: nothing
|
||||
}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-17
@@ -1,4 +1,5 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import type { HaDrawer } from "../../../src/components/ha-drawer";
|
||||
import type { ThemeSettings } from "../../../src/types";
|
||||
import { NAVIGATION_TIMEOUT, SHELL_TIMEOUT } from "../helpers";
|
||||
|
||||
@@ -66,26 +67,34 @@ export async function expectStoredDemoTheme(
|
||||
}
|
||||
|
||||
export async function openDemoSidebar(page: Page) {
|
||||
const menuButton = page.locator("ha-menu-button");
|
||||
if (await menuButton.isVisible()) {
|
||||
const modalDrawer = page.locator("ha-drawer").locator("wa-drawer");
|
||||
await Promise.all([
|
||||
modalDrawer.evaluate(
|
||||
(element) =>
|
||||
new Promise<void>((resolve) => {
|
||||
element.addEventListener("wa-after-show", () => resolve(), {
|
||||
once: true,
|
||||
});
|
||||
})
|
||||
),
|
||||
menuButton.click(),
|
||||
]);
|
||||
const drawer = page.locator("ha-drawer");
|
||||
await expect(drawer).toBeAttached({ timeout: NAVIGATION_TIMEOUT });
|
||||
|
||||
// Pick the layout from ha-drawer, which home-assistant-main sets to "modal"
|
||||
// while committing its very first render (the narrow media query is resolved
|
||||
// synchronously in its constructor). ha-menu-button must not be used for
|
||||
// this: it renders nothing until its Lit contexts resolve, so sampling its
|
||||
// visibility straight after load reports "hidden" on the narrow layout too,
|
||||
// and the drawer would then silently never be opened.
|
||||
const isModal = await drawer.evaluate(
|
||||
(element) => (element as HaDrawer).type === "modal"
|
||||
);
|
||||
|
||||
if (!isModal) {
|
||||
// Wide layout: the sidebar is permanently on screen.
|
||||
await expect(page.locator("ha-sidebar")).toBeVisible({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(page.locator("ha-sidebar")).toBeAttached({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
});
|
||||
const menuButton = page.locator("ha-menu-button");
|
||||
await expect(menuButton).toBeVisible({ timeout: SHELL_TIMEOUT });
|
||||
await menuButton.click();
|
||||
// ha-drawer reflects `open` once the app has actually opened the drawer.
|
||||
// The slide-in animation is covered by Playwright's stability check on the
|
||||
// next interaction.
|
||||
await expect(drawer).toHaveAttribute("open", "", { timeout: SHELL_TIMEOUT });
|
||||
}
|
||||
|
||||
export async function activateDemoSidebarPanel(page: Page, panel: string) {
|
||||
|
||||
Reference in New Issue
Block a user