Compare commits

...

12 Commits

Author SHA1 Message Date
Aidan Timson
e07cf6b52a Fix narrow layouts 2026-04-29 16:03:07 +01:00
Aidan Timson
2ca3378d35 Cleanup 2026-04-29 16:03:07 +01:00
Aidan Timson
95228d23d5 Cleanup 2026-04-29 16:03:07 +01:00
Aidan Timson
05f23e45e1 Use fixed height and constrain editor 2026-04-29 16:03:07 +01:00
Aidan Timson
d6fd9db5c3 Use buffer position and total instead of event id + total in counter 2026-04-29 16:03:07 +01:00
Aidan Timson
4a67b57ef8 Update disclaimer 2026-04-29 16:03:07 +01:00
Aidan Timson
c91040a4d7 Increase bufffer limit to 100, add explainer and tip when rolls over 2026-04-29 16:03:07 +01:00
Aidan Timson
e3e4fb1a08 Show info why only 30 events are keps 2026-04-29 16:03:07 +01:00
Aidan Timson
b78a1218c8 Simplify, remove pinning and auto-follow, stay on event 1 and allow the user to move around manually 2026-04-29 16:03:07 +01:00
Aidan Timson
5d55e0a1b2 Fix 2026-04-29 16:03:07 +01:00
Aidan Timson
9359572f41 Refactor to use pagination 2026-04-29 16:03:07 +01:00
Aidan Timson
34f48acf09 Only show events output when there are any, types and margin 2026-04-29 16:03:07 +01:00
3 changed files with 286 additions and 55 deletions

View File

@@ -18,7 +18,7 @@ import "./events-list";
class HaPanelDevEvent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
@state() private _eventType = "";
@@ -94,6 +94,7 @@ class HaPanelDevEvent extends LitElement {
<event-subscribe-card
.hass=${this.hass}
.narrow=${this.narrow}
.selectedEventType=${this._selectedEventType}
></event-subscribe-card>
</div>
@@ -158,6 +159,8 @@ class HaPanelDevEvent extends LitElement {
padding: var(--ha-space-4);
max-width: 1200px;
margin: auto;
height: 100%;
box-sizing: border-box;
}
:host {
@@ -165,10 +168,26 @@ class HaPanelDevEvent extends LitElement {
-webkit-user-select: initial;
-moz-user-select: initial;
display: block;
height: 100%;
}
:host([narrow]) {
height: auto;
}
:host([narrow]) .content {
height: auto;
}
.flex {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
:host([narrow]) .flex {
min-height: auto;
}
.inputs {
@@ -180,11 +199,19 @@ class HaPanelDevEvent extends LitElement {
}
event-subscribe-card {
display: block;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
margin-top: var(--ha-space-4);
direction: var(--direction);
}
:host([narrow]) event-subscribe-card {
flex: none;
min-height: auto;
}
a {
color: var(--primary-color);
}

View File

@@ -1,21 +1,39 @@
import {
mdiChevronDoubleLeft,
mdiChevronDoubleRight,
mdiChevronLeft,
mdiChevronRight,
mdiInformationOutline,
} from "@mdi/js";
import type { HassEvent } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { formatTime } from "../../../../common/datetime/format_time";
import { formatTimeWithSeconds } from "../../../../common/datetime/format_time";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-yaml-editor";
import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../types";
const MAX_BUFFERED_EVENTS = 100;
interface SubscribedEvent {
id: number;
event: HassEvent;
}
@customElement("event-subscribe-card")
class EventSubscribeCard extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public selectedEventType = "";
@state() private _eventType = "";
@@ -24,13 +42,12 @@ class EventSubscribeCard extends LitElement {
@state() private _eventFilter = "";
@state() private _events: {
id: number;
event: HassEvent;
}[] = [];
@state() private _events: SubscribedEvent[] = [];
@state() private _error?: string;
@state() private _viewedEventId?: number;
private _eventCount = 0;
@state() _ignoredEventsCount = 0;
@@ -113,43 +130,161 @@ class EventSubscribeCard extends LitElement {
</ha-button>
</div>
</ha-card>
<ha-card>
<div class="card-content">
<div class="events">
${repeat(
this._events,
(event) => event.id,
(event) => html`
<div class="event">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
{ name: event.id }
)}
${formatTime(
new Date(event.event.time_fired),
this.hass!.locale,
this.hass!.config
)}:
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${event.event}
read-only
></ha-yaml-editor>
</div>
`
${this._renderEventsCard()}
`;
}
private _renderEventsCard(): TemplateResult {
if (!this._events.length) {
const message = this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.waiting_for_events"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_prompt"
);
return html`
<ha-card class="events-card">
<div class="empty-state">${message}</div>
</ha-card>
`;
}
const bufferTotal = this._events.length;
const index = this._resolveViewedIndex();
const event = this._events[index];
const position = event.id + 1;
const bufferPosition = bufferTotal - index;
const atNewest = index === 0;
const hasRolledOver = this._events[bufferTotal - 1].id > 0;
return html`
<ha-card class="events-card">
<div class="events-toolbar">
<ha-icon-button
.path=${mdiChevronDoubleLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.oldest_event"
)}
@click=${this._showOldest}
></ha-icon-button>
<ha-icon-button
.path=${mdiChevronLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.older_event"
)}
@click=${this._showOlder}
></ha-icon-button>
<div class="event-info">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
{
name: position,
time: formatTimeWithSeconds(
new Date(event.event.time_fired),
this.hass!.locale,
this.hass!.config
),
}
)}
<span class="counter">(${bufferPosition} / ${bufferTotal})</span>
${hasRolledOver
? html`
<ha-svg-icon
id="buffer-info"
class="buffer-info"
.path=${mdiInformationOutline}
></ha-svg-icon>
<ha-tooltip for="buffer-info" placement="bottom">
<span class="buffer-tooltip">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.buffer_disclaimer",
{ count: MAX_BUFFERED_EVENTS }
)}
</span>
</ha-tooltip>
`
: nothing}
</div>
<ha-icon-button
.path=${mdiChevronRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newer_event"
)}
@click=${this._showNewer}
></ha-icon-button>
<ha-icon-button
.path=${mdiChevronDoubleRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newest_event"
)}
@click=${this._showNewest}
></ha-icon-button>
</div>
<ha-yaml-editor
.hass=${this.hass}
.value=${event.event}
auto-update
read-only
></ha-yaml-editor>
</ha-card>
`;
}
private _valueChanged(ev: InputEvent): void {
private _resolveViewedIndex(): number {
if (this._viewedEventId === undefined) {
return 0;
}
const found = this._events.findIndex((e) => e.id === this._viewedEventId);
// Fall back to the oldest available event when the viewed one has aged out.
return found === -1 ? this._events.length - 1 : found;
}
private _showOldest() {
if (!this._events.length) {
return;
}
this._viewedEventId = this._events[this._events.length - 1].id;
}
private _showOlder() {
if (!this._events.length) {
return;
}
const next = Math.min(
this._resolveViewedIndex() + 1,
this._events.length - 1
);
this._viewedEventId = this._events[next].id;
}
private _showNewest() {
if (!this._events.length) {
return;
}
this._viewedEventId = this._events[0].id;
}
private _showNewer() {
if (!this._events.length) {
return;
}
const next = Math.max(this._resolveViewedIndex() - 1, 0);
this._viewedEventId = this._events[next].id;
}
private _valueChanged(ev: InputEvent) {
this._eventType = (ev.target as HaInput).value ?? "";
this._error = undefined;
}
private _filterChanged(ev: InputEvent): void {
private _filterChanged(ev: InputEvent) {
this._eventFilter = (ev.target as HaInput).value ?? "";
}
@@ -160,7 +295,7 @@ class EventSubscribeCard extends LitElement {
const searchStr = this._eventFilter;
function visit(node) {
function visit(node: unknown) {
// Handle primitives directly
if (node === null || typeof node !== "object") {
return String(node).includes(searchStr);
@@ -203,55 +338,116 @@ class EventSubscribeCard extends LitElement {
return;
}
const tail =
this._events.length > 30
? this._events.slice(0, 29)
this._events.length >= MAX_BUFFERED_EVENTS
? this._events.slice(0, MAX_BUFFERED_EVENTS - 1)
: this._events;
const id = this._eventCount++;
this._events = [
{
event,
id: this._eventCount++,
id,
},
...tail,
];
if (this._viewedEventId === undefined) {
this._viewedEventId = id;
}
}, this._eventType);
} catch (error: any) {
} catch (error) {
this._error = this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_failed",
{ error: error.message || "Unknown error" }
{
error:
error instanceof Error
? error.message
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.unknown_error"
),
}
);
}
}
}
private _clearEvents(): void {
private _clearEvents() {
this._events = [];
this._eventCount = 0;
this._ignoredEventsCount = 0;
this._error = undefined;
this._viewedEventId = undefined;
}
static styles = css`
:host {
display: flex;
flex-direction: column;
min-height: 0;
}
ha-input {
margin-bottom: var(--ha-space-2);
}
.error-message {
margin-top: var(--ha-space-2);
}
.event {
border-top: 1px solid var(--divider-color);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.event:last-child {
border-bottom: 0;
margin-bottom: 0;
}
pre {
font-family: var(--ha-font-family-code);
}
ha-card {
margin-bottom: var(--ha-space-1);
margin-bottom: var(--ha-space-2);
}
.events-card {
display: flex;
flex-direction: column;
height: 620px;
padding: var(--ha-space-2);
}
:host([narrow]) .events-card {
height: auto;
min-height: 360px;
}
.events-toolbar {
display: flex;
align-items: center;
gap: var(--ha-space-2);
}
.empty-state {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: var(--ha-space-8);
color: var(--primary-text-color);
text-align: center;
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-normal);
}
.event-info {
flex: 1;
text-align: center;
font-size: var(--ha-font-size-m);
}
.counter {
color: var(--secondary-text-color);
margin-left: var(--ha-space-2);
}
.buffer-info {
color: var(--secondary-text-color);
margin-left: var(--ha-space-1);
vertical-align: middle;
--mdc-icon-size: 16px;
}
.buffer-tooltip {
white-space: pre-line;
display: block;
max-width: 320px;
}
ha-yaml-editor {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
margin-top: var(--ha-space-2);
--code-mirror-height: 100%;
}
`;
}

View File

@@ -3781,7 +3781,7 @@
"type": "Event type",
"data": "Event data (YAML, optional)",
"fire_event": "Fire event",
"event_fired": "Event {name} fired",
"event_fired": "Event {name} fired at {time}",
"active_listeners": "Active listeners",
"count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})",
"listen_to_events": "Listen to events",
@@ -3795,7 +3795,15 @@
"clear_events": "Clear events",
"alert_event_type": "Event type is a mandatory field",
"notification_event_fired": "Event {type} successfully fired!",
"subscribe_failed": "Failed to subscribe to event: {error}"
"subscribe_failed": "Failed to subscribe to event: {error}",
"unknown_error": "Unknown error",
"oldest_event": "Oldest event",
"older_event": "Older event",
"newer_event": "Newer event",
"newest_event": "Newest event",
"waiting_for_events": "Waiting for events…",
"subscribe_prompt": "Subscribe to an event type above to see events here.",
"buffer_disclaimer": "Showing the latest {count} events. Older events are discarded as new ones arrive.\n\nTip: Subscribe to a more specific event type, or use the filter to narrow results."
},
"actions": {
"title": "Actions",