diff --git a/package.json b/package.json
index 17670b2b1b..873013d82f 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"leaflet": "^1.4.0",
"lit-element": "^2.2.1",
"lit-html": "^1.1.0",
+ "lit-virtualizer": "^0.4.2",
"marked": "^0.6.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
diff --git a/src/data/logbook.ts b/src/data/logbook.ts
new file mode 100644
index 0000000000..be4ab3166e
--- /dev/null
+++ b/src/data/logbook.ts
@@ -0,0 +1,7 @@
+export interface LogbookEntry {
+ when: string;
+ name: string;
+ message: string;
+ entity_id?: string;
+ domain: string;
+}
diff --git a/src/panels/logbook/ha-logbook.js b/src/panels/logbook/ha-logbook.js
deleted file mode 100644
index 3faa6be64f..0000000000
--- a/src/panels/logbook/ha-logbook.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import "@polymer/iron-flex-layout/iron-flex-layout-classes";
-import "@polymer/iron-icon/iron-icon";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-
-import formatTime from "../../common/datetime/format_time";
-import formatDate from "../../common/datetime/format_date";
-import { EventsMixin } from "../../mixins/events-mixin";
-import LocalizeMixin from "../../mixins/localize-mixin";
-import { domainIcon } from "../../common/entity/domain_icon";
-import { computeRTL } from "../../common/util/compute_rtl";
-
-/*
- * @appliesMixin EventsMixin
- */
-class HaLogbook extends LocalizeMixin(EventsMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
-
- [[localize('ui.panel.logbook.entries_not_found')]]
-
-
-
-
- [[_formatDate(item.when)]]
-
-
-
-
[[_formatTime(item.when)]]
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: {
- type: Object,
- },
-
- entries: {
- type: Array,
- value: [],
- },
- rtl: {
- type: Boolean,
- reflectToAttribute: true,
- computed: "_computeRTL(hass)",
- },
- };
- }
-
- _formatTime(date) {
- return formatTime(new Date(date), this.hass.language);
- }
-
- _formatDate(date) {
- return formatDate(new Date(date), this.hass.language);
- }
-
- _needHeader(change, index) {
- if (!index) return true;
- const current = this.get("when", change.base[index]);
- const previous = this.get("when", change.base[index - 1]);
- return (
- current &&
- previous &&
- new Date(current).toDateString() !== new Date(previous).toDateString()
- );
- }
-
- _computeIcon(domain) {
- return domainIcon(domain);
- }
-
- _computeRTL(hass) {
- return computeRTL(hass);
- }
-
- entityClicked(ev) {
- ev.preventDefault();
- this.fire("hass-more-info", { entityId: ev.model.item.entity_id });
- }
-}
-
-customElements.define("ha-logbook", HaLogbook);
diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts
new file mode 100644
index 0000000000..158211b045
--- /dev/null
+++ b/src/panels/logbook/ha-logbook.ts
@@ -0,0 +1,156 @@
+import "@polymer/iron-icon/iron-icon";
+
+import formatTime from "../../common/datetime/format_time";
+import formatDate from "../../common/datetime/format_date";
+import { domainIcon } from "../../common/entity/domain_icon";
+import { computeRTL } from "../../common/util/compute_rtl";
+import {
+ LitElement,
+ html,
+ property,
+ TemplateResult,
+ CSSResult,
+ css,
+ PropertyValues,
+} from "lit-element";
+import { HomeAssistant } from "../../types";
+import { fireEvent } from "../../common/dom/fire_event";
+import "lit-virtualizer";
+import { LogbookEntry } from "../../data/logbook";
+
+class HaLogbook extends LitElement {
+ @property() public hass!: HomeAssistant;
+ @property() public entries: LogbookEntry[] = [];
+ @property({ attribute: "rtl", type: Boolean, reflect: true })
+ // @ts-ignore
+ private _rtl = false;
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+ if (!changedProps.has("hass")) {
+ return;
+ }
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ if (oldHass && oldHass.language !== this.hass.language) {
+ this._rtl = computeRTL(this.hass);
+ }
+ }
+
+ protected firstUpdated(changedProps: PropertyValues) {
+ super.firstUpdated(changedProps);
+ this._rtl = computeRTL(this.hass);
+ }
+
+ protected render(): TemplateResult | void {
+ if (!this.entries?.length) {
+ return html`
+ ${this.hass.localize("ui.panel.logbook.entries_not_found")}
+ `;
+ }
+
+ return html`
+
+ this._renderLogbookItem(item, index)}
+ style="height: 100%;"
+ >
+ `;
+ }
+
+ private _renderLogbookItem(
+ item: LogbookEntry,
+ index: number
+ ): TemplateResult {
+ const previous = this.entries[index - 1];
+ return html`
+
+ ${index === 0 ||
+ (item?.when &&
+ previous?.when &&
+ new Date(item.when).toDateString() !==
+ new Date(previous.when).toDateString())
+ ? html`
+
+ ${formatDate(new Date(item.when), this.hass.language)}
+
+ `
+ : html``}
+
+
+
+ ${formatTime(new Date(item.when), this.hass.language)}
+
+
+
+ ${!item.entity_id
+ ? html`
+
${item.name}
+ `
+ : html`
+
+ ${item.name}
+
+ `}
+
${item.message}
+
+
+
+ `;
+ }
+
+ private _entityClicked(ev: Event) {
+ ev.preventDefault();
+ fireEvent(this, "hass-more-info", {
+ entityId: (ev.target as any).entityId,
+ });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ display: block;
+ height: 100%;
+ }
+
+ :host([rtl]) {
+ direction: ltr;
+ }
+
+ .entry {
+ display: flex;
+ line-height: 2em;
+ }
+
+ .time {
+ width: 55px;
+ font-size: 0.8em;
+ color: var(--secondary-text-color);
+ }
+
+ :host([rtl]) .date {
+ direction: rtl;
+ }
+
+ iron-icon {
+ margin: 0 8px 0 16px;
+ color: var(--primary-text-color);
+ }
+
+ .message {
+ color: var(--primary-text-color);
+ }
+
+ a {
+ color: var(--primary-color);
+ }
+ `;
+ }
+}
+
+customElements.define("ha-logbook", HaLogbook);
diff --git a/src/panels/logbook/ha-panel-logbook.js b/src/panels/logbook/ha-panel-logbook.js
index 7e46b66979..1f72e8ccba 100644
--- a/src/panels/logbook/ha-panel-logbook.js
+++ b/src/panels/logbook/ha-panel-logbook.js
@@ -28,7 +28,15 @@ class HaPanelLogbook extends LocalizeMixin(PolymerElement) {
return html`