refactor: Refactor internal error / info message components (#1842)
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
import {
|
import { FrigateCardError, Message, MessageType } from '../types';
|
||||||
FrigateCardError,
|
|
||||||
MESSAGE_TYPE_PRIORITIES,
|
|
||||||
Message,
|
|
||||||
MessageType,
|
|
||||||
} from '../types';
|
|
||||||
import { errorToConsole } from '../utils/basic';
|
import { errorToConsole } from '../utils/basic';
|
||||||
import { CardMessageAPI } from './types';
|
import { CardMessageAPI } from './types';
|
||||||
|
|
||||||
|
type MessagePriority = {
|
||||||
|
[type in MessageType]: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MESSAGE_TYPE_PRIORITIES: MessagePriority = {
|
||||||
|
info: 10,
|
||||||
|
error: 20,
|
||||||
|
connection: 30,
|
||||||
|
diagnostics: 40,
|
||||||
|
};
|
||||||
|
|
||||||
export class MessageManager {
|
export class MessageManager {
|
||||||
protected _message: Message | null = null;
|
protected _message: Message | null = null;
|
||||||
protected _api: CardMessageAPI;
|
protected _api: CardMessageAPI;
|
||||||
@@ -52,17 +58,20 @@ export class MessageManager {
|
|||||||
|
|
||||||
errorToConsole(error);
|
errorToConsole(error);
|
||||||
this.setMessageIfHigherPriority({
|
this.setMessageIfHigherPriority({
|
||||||
message: prefix ? `${prefix}: ${error.message}` : error.message,
|
message: prefix ? `${prefix}: ${error.message}` : String(error.message),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
...(error instanceof FrigateCardError && { context: error.context }),
|
...(error instanceof FrigateCardError && { context: error.context }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setMessageIfHigherPriority(message: Message): boolean {
|
public setMessageIfHigherPriority(message: Message): boolean {
|
||||||
|
const resolveMessageType = (message: Message): MessageType => {
|
||||||
|
return message.type ?? 'info';
|
||||||
|
};
|
||||||
const currentPriority = this._message
|
const currentPriority = this._message
|
||||||
? MESSAGE_TYPE_PRIORITIES[this._message.type]
|
? MESSAGE_TYPE_PRIORITIES[resolveMessageType(this._message)]
|
||||||
: 0;
|
: 0;
|
||||||
const newPriority = MESSAGE_TYPE_PRIORITIES[message.type];
|
const newPriority = MESSAGE_TYPE_PRIORITIES[resolveMessageType(message)];
|
||||||
|
|
||||||
if (this._message && newPriority < currentPriority) {
|
if (this._message && newPriority < currentPriority) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { LitElement, ReactiveController } from 'lit';
|
import { LitElement, ReactiveController } from 'lit';
|
||||||
import { FrigateCardMessageEventTarget } from '../../components/message.js';
|
|
||||||
import { MediaLoadedInfo } from '../../types.js';
|
import { MediaLoadedInfo } from '../../types.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardMediaLoadedEventTarget,
|
FrigateCardMediaLoadedEventTarget,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
} from '../../utils/media-info.js';
|
} from '../../utils/media-info.js';
|
||||||
|
import { FrigateCardMessageEventTarget } from '../message/dispatch.js';
|
||||||
|
|
||||||
interface LiveViewContext {
|
interface LiveViewContext {
|
||||||
// A cameraID override (used for dependencies/substreams to force a different
|
// A cameraID override (used for dependencies/substreams to force a different
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { TROUBLESHOOTING_URL } from '../../const';
|
||||||
|
import { Message } from '../../types';
|
||||||
|
|
||||||
|
export class MessageController {
|
||||||
|
public getMessageString(message: Message): string {
|
||||||
|
return (
|
||||||
|
message.message +
|
||||||
|
(message.context && typeof message.context === 'string'
|
||||||
|
? ': ' + message.context
|
||||||
|
: '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIcon(message: Message): string {
|
||||||
|
return message.icon
|
||||||
|
? message.icon
|
||||||
|
: message.type === 'error'
|
||||||
|
? 'mdi:alert-circle'
|
||||||
|
: 'mdi:information-outline';
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldShowTroubleshootingURL(message: Message): boolean {
|
||||||
|
return message.type === 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTroubleshootingURL(message: Message): string {
|
||||||
|
return message.troubleshootingURL ?? TROUBLESHOOTING_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getContextStrings(message: Message): string[] {
|
||||||
|
if (Array.isArray(message.context)) {
|
||||||
|
return message.context.map((contextItem) => yaml.dump(contextItem));
|
||||||
|
}
|
||||||
|
if (typeof message.context === 'object') {
|
||||||
|
return [yaml.dump(message.context)];
|
||||||
|
}
|
||||||
|
if (typeof message.context === 'string') {
|
||||||
|
return [message.context];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { FrigateCardError, Message } from '../../types';
|
||||||
|
import { dispatchFrigateCardEvent } from '../../utils/basic';
|
||||||
|
|
||||||
|
// Facilitates correct typing of event handlers.
|
||||||
|
export interface FrigateCardMessageEventTarget extends EventTarget {
|
||||||
|
addEventListener(
|
||||||
|
event: 'frigate-card:message',
|
||||||
|
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => void,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
event: 'frigate-card:message',
|
||||||
|
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => void,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an event with an error message to show to the user. Calling this
|
||||||
|
* method will grind the card to a halt, so should only be used for "global" /
|
||||||
|
* critical errors (i.e. not for individual errors with a given camera, since
|
||||||
|
* there may be multiple correctly functioning cameras in a grid).
|
||||||
|
* @param element The element to send the event.
|
||||||
|
* @param message The message to show.
|
||||||
|
*/
|
||||||
|
export const dispatchFrigateCardErrorEvent = (
|
||||||
|
element: EventTarget,
|
||||||
|
error: unknown,
|
||||||
|
): void => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
dispatchFrigateCardEvent<Message>(element, 'message', {
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
...(error instanceof FrigateCardError && { context: error.context }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -41,8 +41,7 @@ export class FrigateCardDiagnostics extends LitElement {
|
|||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return renderMessage({
|
return renderMessage({
|
||||||
message: localize('error.diagnostics'),
|
message: localize('error.diagnostics'),
|
||||||
type: 'diagnostics',
|
icon: 'mdi:cogs',
|
||||||
icon: 'mdi:information',
|
|
||||||
context: this._diagnostics,
|
context: this._diagnostics,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ConditionsManagerEpoch,
|
ConditionsManagerEpoch,
|
||||||
evaluateConditionViaEvent,
|
evaluateConditionViaEvent,
|
||||||
} from '../card-controller/conditions-manager.js';
|
} from '../card-controller/conditions-manager.js';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||||
import {
|
import {
|
||||||
FrigateConditional,
|
FrigateConditional,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
@@ -29,7 +30,6 @@ import { localize } from '../localize/localize.js';
|
|||||||
import elementsStyle from '../scss/elements.scss';
|
import elementsStyle from '../scss/elements.scss';
|
||||||
import { FrigateCardError } from '../types.js';
|
import { FrigateCardError } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries'
|
|||||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
import './media-filter';
|
import './media-filter';
|
||||||
import { renderMessage, renderProgressIndicator } from './message.js';
|
import './message.js';
|
||||||
|
import { renderMessage } from './message.js';
|
||||||
|
import './progress-indicator.js';
|
||||||
|
import { renderProgressIndicator } from './progress-indicator.js';
|
||||||
import './surround-basic';
|
import './surround-basic';
|
||||||
import './thumbnail.js';
|
import './thumbnail.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from '../../components-lib/message/dispatch.js';
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +43,6 @@ import { getTextDirection } from '../../utils/text-direction.js';
|
|||||||
import { View } from '../../view/view.js';
|
import { View } from '../../view/view.js';
|
||||||
import '../carousel';
|
import '../carousel';
|
||||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from '../message.js';
|
|
||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
import '../ptz.js';
|
import '../ptz.js';
|
||||||
import { FrigateCardPTZ } from '../ptz.js';
|
import { FrigateCardPTZ } from '../ptz.js';
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import {
|
|||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
} from '../../../utils/media-info.js';
|
} from '../../../utils/media-info.js';
|
||||||
import { Timer } from '../../../utils/timer.js';
|
import { Timer } from '../../../utils/timer.js';
|
||||||
import { renderMessage, renderProgressIndicator } from '../../message.js';
|
import '../../message.js';
|
||||||
|
import { renderMessage } from '../../message.js';
|
||||||
|
import '../../progress-indicator.js';
|
||||||
|
import { renderProgressIndicator } from '../../progress-indicator.js';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ import {
|
|||||||
} from '../../../utils/media.js';
|
} from '../../../utils/media.js';
|
||||||
import { screenshotMedia } from '../../../utils/screenshot.js';
|
import { screenshotMedia } from '../../../utils/screenshot.js';
|
||||||
import { renderTask } from '../../../utils/task.js';
|
import { renderTask } from '../../../utils/task.js';
|
||||||
import { renderMessage, renderProgressIndicator } from '../../message.js';
|
import '../../message.js';
|
||||||
|
import { renderMessage } from '../../message.js';
|
||||||
|
import '../../progress-indicator.js';
|
||||||
|
import { renderProgressIndicator } from '../../progress-indicator.js';
|
||||||
import { VideoRTC } from './go2rtc/video-rtc.js';
|
import { VideoRTC } from './go2rtc/video-rtc.js';
|
||||||
|
|
||||||
// Create a wrapper for AlexxIT's WebRTC card
|
// Create a wrapper for AlexxIT's WebRTC card
|
||||||
|
|||||||
+26
-216
@@ -1,39 +1,39 @@
|
|||||||
import yaml from 'js-yaml';
|
|
||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { ref, Ref } from 'lit/directives/ref.js';
|
import { MessageController } from '../components-lib/message/controller.js';
|
||||||
import { CardWideConfig } from '../config/types.js';
|
|
||||||
import { TROUBLESHOOTING_URL } from '../const.js';
|
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import messageStyle from '../scss/message.scss';
|
import messageStyle from '../scss/message.scss';
|
||||||
import { FrigateCardError, Message, MessageType } from '../types.js';
|
import { Message } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
|
||||||
import './icon.js';
|
import './icon.js';
|
||||||
|
|
||||||
|
export function renderMessage(message: Message | null): TemplateResult {
|
||||||
|
return html` <frigate-card-message .message=${message}></frigate-card-message>`;
|
||||||
|
}
|
||||||
@customElement('frigate-card-message')
|
@customElement('frigate-card-message')
|
||||||
export class FrigateCardMessage extends LitElement {
|
export class FrigateCardMessage extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public message: string | TemplateResult<1> = '';
|
public message?: Message;
|
||||||
|
|
||||||
@property({ attribute: false })
|
private _controller = new MessageController();
|
||||||
public context?: unknown;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
protected render(): TemplateResult | void {
|
||||||
public icon?: string;
|
if (!this.message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@property({ attribute: true, type: Boolean })
|
const messageTemplate = html`
|
||||||
public dotdotdot?: boolean;
|
${this._controller.getMessageString(this.message)}.
|
||||||
|
${this._controller.shouldShowTroubleshootingURL(this.message)
|
||||||
|
? html`<a href="${this._controller.getTroubleshootingURL(this.message)}"
|
||||||
|
>${localize('error.troubleshooting')}</a
|
||||||
|
>`
|
||||||
|
: ''}
|
||||||
|
`;
|
||||||
|
|
||||||
// Render the menu.
|
const icon = this._controller.getIcon(this.message);
|
||||||
protected render(): TemplateResult {
|
|
||||||
const icon = this.icon ? this.icon : 'mdi:information-outline';
|
|
||||||
const classes = {
|
const classes = {
|
||||||
dotdotdot: !!this.dotdotdot,
|
dotdotdot: !!this.message?.dotdotdot,
|
||||||
};
|
|
||||||
|
|
||||||
const renderContext = (contextItem: unknown): TemplateResult => {
|
|
||||||
return html`<pre>${yaml.dump(contextItem)}</pre>`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return html` <div class="wrapper">
|
return html` <div class="wrapper">
|
||||||
@@ -42,18 +42,10 @@ export class FrigateCardMessage extends LitElement {
|
|||||||
<frigate-card-icon .icon="${{ icon: icon }}"></frigate-card-icon>
|
<frigate-card-icon .icon="${{ icon: icon }}"></frigate-card-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="contents">
|
<div class="contents">
|
||||||
<span class="${classMap(classes)}">
|
<span class="${classMap(classes)}">${messageTemplate}</span>
|
||||||
${this.message
|
${this._controller
|
||||||
? html`${this.message}${this.context && typeof this.context === 'string'
|
.getContextStrings(this.message)
|
||||||
? ': ' + this.context
|
.map((contextItem) => html`<pre>${contextItem}</pre>`)}
|
||||||
: ''}`
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
${this.context && Array.isArray(this.context)
|
|
||||||
? this.context.map((contextItem) => renderContext(contextItem))
|
|
||||||
: typeof this.context === 'object'
|
|
||||||
? renderContext(this.context)
|
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -64,190 +56,8 @@ export class FrigateCardMessage extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement('frigate-card-error-message')
|
|
||||||
export class FrigateCardErrorMessage extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public message?: Message;
|
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
|
||||||
if (!this.message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return html` <frigate-card-message
|
|
||||||
.message=${html` ${this.message.message}.
|
|
||||||
<a href="${TROUBLESHOOTING_URL}"> ${localize('error.troubleshooting')}</a>.`}
|
|
||||||
.icon=${this.message.icon ?? 'mdi:alert-circle'}
|
|
||||||
.context=${this.message.context}
|
|
||||||
.dotdotdot=${this.message.dotdotdot}
|
|
||||||
>
|
|
||||||
</frigate-card-message>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(messageStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large';
|
|
||||||
|
|
||||||
@customElement('frigate-card-progress-indicator')
|
|
||||||
export class FrigateCardProgressIndicator extends LitElement {
|
|
||||||
@property({ attribute: false })
|
|
||||||
public message: string | TemplateResult = '';
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public animated = false;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public size: FrigateCardProgressIndicatorSize = 'large';
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html` <div class="message vertical">
|
|
||||||
${this.animated
|
|
||||||
? html`<ha-circular-progress indeterminate size="${this.size}">
|
|
||||||
</ha-circular-progress>`
|
|
||||||
: html`<frigate-card-icon
|
|
||||||
.icon=${{ icon: 'mdi:timer-sand' }}
|
|
||||||
></frigate-card-icon>`}
|
|
||||||
${this.message ? html`<span>${this.message}</span>` : html``}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return unsafeCSS(messageStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderMessage(message: Message | null): TemplateResult {
|
|
||||||
if (message?.type === 'error') {
|
|
||||||
return html` <frigate-card-error-message
|
|
||||||
.message=${message}
|
|
||||||
></frigate-card-error-message>`;
|
|
||||||
} else if (message) {
|
|
||||||
return html` <frigate-card-message
|
|
||||||
.message=${message.message}
|
|
||||||
.icon=${message.icon}
|
|
||||||
.context=${message.context}
|
|
||||||
.dotdotdot=${message.dotdotdot}
|
|
||||||
></frigate-card-message>`;
|
|
||||||
}
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderProgressIndicator(options?: {
|
|
||||||
message?: string;
|
|
||||||
cardWideConfig?: CardWideConfig | null;
|
|
||||||
componentRef?: Ref<HTMLElement>;
|
|
||||||
classes?: ClassInfo;
|
|
||||||
size?: FrigateCardProgressIndicatorSize;
|
|
||||||
}): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<frigate-card-progress-indicator
|
|
||||||
class="${classMap(options?.classes ?? {})}"
|
|
||||||
.size=${options?.size}
|
|
||||||
${options?.componentRef ? ref(options.componentRef) : ''}
|
|
||||||
.message=${options?.message || ''}
|
|
||||||
.animated=${options?.cardWideConfig?.performance?.features
|
|
||||||
.animated_progress_indicator ?? true}
|
|
||||||
>
|
|
||||||
</frigate-card-progress-indicator>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch an event with a message to show to the user. Calling this method
|
|
||||||
* will grind the card to a halt, so should only be used for "global" / critical
|
|
||||||
* errors (i.e. not for individual errors with a given camera, since there may
|
|
||||||
* be multiple correctly functioning cameras in a grid).
|
|
||||||
* @param element The element to send the event.
|
|
||||||
* @param message The message to show.
|
|
||||||
* @param options Optional icon and context to include.
|
|
||||||
*/
|
|
||||||
function dispatchMessageEvent(
|
|
||||||
element: EventTarget,
|
|
||||||
message: string,
|
|
||||||
type: MessageType,
|
|
||||||
options?: {
|
|
||||||
icon?: string;
|
|
||||||
context?: unknown;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
dispatchFrigateCardEvent<Message>(element, 'message', {
|
|
||||||
message: message,
|
|
||||||
type: type,
|
|
||||||
icon: options?.icon,
|
|
||||||
context: options?.context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch an event with an error message to show to the user. Calling this
|
|
||||||
* method will grind the card to a halt, so should only be used for "global" /
|
|
||||||
* critical errors (i.e. not for individual errors with a given camera, since
|
|
||||||
* there may be multiple correctly functioning cameras in a grid).
|
|
||||||
* @param element The element to send the event.
|
|
||||||
* @param message The message to show.
|
|
||||||
* @param options Optional context to include.
|
|
||||||
*/
|
|
||||||
function dispatchErrorMessageEvent(
|
|
||||||
element: EventTarget,
|
|
||||||
message: string,
|
|
||||||
options?: {
|
|
||||||
context?: unknown;
|
|
||||||
},
|
|
||||||
): void {
|
|
||||||
dispatchMessageEvent(element, message, 'error', {
|
|
||||||
context: options?.context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch an event with an error message to show to the user. Calling this
|
|
||||||
* method will grind the card to a halt, so should only be used for "global" /
|
|
||||||
* critical errors (i.e. not for individual errors with a given camera, since
|
|
||||||
* there may be multiple correctly functioning cameras in a grid).
|
|
||||||
* @param element The element to send the event.
|
|
||||||
* @param message The message to show.
|
|
||||||
*/
|
|
||||||
export function dispatchFrigateCardErrorEvent(
|
|
||||||
element: EventTarget,
|
|
||||||
error: unknown,
|
|
||||||
): void {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
dispatchErrorMessageEvent(element, error.message, {
|
|
||||||
...(error instanceof FrigateCardError && { context: error.context }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Facilitates correct typing of event handlers.
|
|
||||||
export interface FrigateCardMessageEventTarget extends EventTarget {
|
|
||||||
addEventListener(
|
|
||||||
event: 'frigate-card:message',
|
|
||||||
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => void,
|
|
||||||
options?: AddEventListenerOptions | boolean,
|
|
||||||
): void;
|
|
||||||
addEventListener(
|
|
||||||
type: string,
|
|
||||||
callback: EventListenerOrEventListenerObject,
|
|
||||||
options?: AddEventListenerOptions | boolean,
|
|
||||||
): void;
|
|
||||||
removeEventListener(
|
|
||||||
event: 'frigate-card:message',
|
|
||||||
listener: (this: FrigateCardMessageEventTarget, ev: CustomEvent<Message>) => void,
|
|
||||||
options?: boolean | EventListenerOptions,
|
|
||||||
): void;
|
|
||||||
removeEventListener(
|
|
||||||
type: string,
|
|
||||||
callback: EventListenerOrEventListenerObject,
|
|
||||||
options?: boolean | EventListenerOptions,
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
|
|
||||||
'frigate-card-error-message': FrigateCardErrorMessage;
|
|
||||||
'frigate-card-message': FrigateCardMessage;
|
'frigate-card-message': FrigateCardMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { CardWideConfig } from '../config/types.js';
|
||||||
|
import messageStyle from '../scss/message.scss';
|
||||||
|
import './icon.js';
|
||||||
|
|
||||||
|
type FrigateCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large';
|
||||||
|
|
||||||
|
export function renderProgressIndicator(options?: {
|
||||||
|
message?: string;
|
||||||
|
cardWideConfig?: CardWideConfig | null;
|
||||||
|
componentRef?: Ref<HTMLElement>;
|
||||||
|
classes?: ClassInfo;
|
||||||
|
size?: FrigateCardProgressIndicatorSize;
|
||||||
|
}): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<frigate-card-progress-indicator
|
||||||
|
class="${classMap(options?.classes ?? {})}"
|
||||||
|
.size=${options?.size}
|
||||||
|
${options?.componentRef ? ref(options.componentRef) : ''}
|
||||||
|
.message=${options?.message || ''}
|
||||||
|
.animated=${options?.cardWideConfig?.performance?.features
|
||||||
|
.animated_progress_indicator ?? true}
|
||||||
|
>
|
||||||
|
</frigate-card-progress-indicator>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('frigate-card-progress-indicator')
|
||||||
|
export class FrigateCardProgressIndicator extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public message: string = '';
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public animated = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public size: FrigateCardProgressIndicatorSize = 'large';
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html` <div class="message vertical">
|
||||||
|
${this.animated
|
||||||
|
? html`<ha-circular-progress indeterminate size="${this.size}">
|
||||||
|
</ha-circular-progress>`
|
||||||
|
: html`<frigate-card-icon
|
||||||
|
.icon=${{ icon: 'mdi:timer-sand' }}
|
||||||
|
></frigate-card-icon>`}
|
||||||
|
${this.message ? html`<span>${this.message}</span>` : html``}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(messageStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { CameraManager } from '../camera-manager/manager.js';
|
|||||||
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
||||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||||
import thumbnailFeatureTextStyle from '../scss/thumbnail-feature-text.scss';
|
import thumbnailFeatureTextStyle from '../scss/thumbnail-feature-text.scss';
|
||||||
@@ -32,7 +33,6 @@ import { renderTask } from '../utils/task.js';
|
|||||||
import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js';
|
import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js';
|
||||||
import { ViewMediaClassifier } from '../view/media-classifier.js';
|
import { ViewMediaClassifier } from '../view/media-classifier.js';
|
||||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
|
||||||
|
|
||||||
// The minimum width of a thumbnail with details enabled.
|
// The minimum width of a thumbnail with details enabled.
|
||||||
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import { screenshotMedia } from '../../utils/screenshot.js';
|
|||||||
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
||||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
||||||
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
||||||
import { renderProgressIndicator } from '../message.js';
|
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||||
|
|
||||||
@customElement('frigate-card-viewer-provider')
|
@customElement('frigate-card-viewer-provider')
|
||||||
export class FrigateCardViewerProvider
|
export class FrigateCardViewerProvider
|
||||||
|
|||||||
+3
-13
@@ -45,23 +45,13 @@ export interface MediaLoadedInfo {
|
|||||||
|
|
||||||
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
|
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
|
||||||
|
|
||||||
type MessagePriority = {
|
|
||||||
[type in MessageType]: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MESSAGE_TYPE_PRIORITIES: MessagePriority = {
|
|
||||||
info: 10,
|
|
||||||
error: 20,
|
|
||||||
connection: 30,
|
|
||||||
diagnostics: 40,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
message: unknown;
|
message: string;
|
||||||
type: MessageType;
|
type?: MessageType;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
context?: unknown;
|
context?: unknown;
|
||||||
dotdotdot?: boolean;
|
dotdotdot?: boolean;
|
||||||
|
troubleshootingURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrigateCardMediaPlayer {
|
export interface FrigateCardMediaPlayer {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { Task } from '@lit-labs/task';
|
import { Task } from '@lit-labs/task';
|
||||||
import { html, TemplateResult } from 'lit';
|
import { html, TemplateResult } from 'lit';
|
||||||
import { renderProgressIndicator } from '../components/message';
|
import { renderProgressIndicator } from '../components/progress-indicator';
|
||||||
import { CardWideConfig } from '../config/types';
|
import { CardWideConfig } from '../config/types';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createCardAPI } from '../test-utils';
|
|||||||
const createMessage = (options?: Partial<Message>): Message => {
|
const createMessage = (options?: Partial<Message>): Message => {
|
||||||
return {
|
return {
|
||||||
message: options?.message ?? 'message',
|
message: options?.message ?? 'message',
|
||||||
type: options?.type ?? 'info',
|
...(!!options?.type && { type: options.type }),
|
||||||
...(!!options?.icon && { icon: options.icon }),
|
...(!!options?.icon && { icon: options.icon }),
|
||||||
...(!!options?.context && { context: options.context }),
|
...(!!options?.context && { context: options.context }),
|
||||||
...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }),
|
...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }),
|
||||||
@@ -97,8 +97,11 @@ describe('MessageManager', () => {
|
|||||||
const errorMessage = createMessage({ type: 'error' });
|
const errorMessage = createMessage({ type: 'error' });
|
||||||
manager.setMessageIfHigherPriority(errorMessage);
|
manager.setMessageIfHigherPriority(errorMessage);
|
||||||
|
|
||||||
const infoMessage = createMessage({ type: 'info' });
|
const explicitInfoMessage = createMessage({ type: 'info' });
|
||||||
manager.setMessageIfHigherPriority(infoMessage);
|
manager.setMessageIfHigherPriority(explicitInfoMessage);
|
||||||
|
|
||||||
|
const implicitInfoMessage = createMessage();
|
||||||
|
manager.setMessageIfHigherPriority(implicitInfoMessage);
|
||||||
|
|
||||||
expect(manager.getMessage()).toBe(errorMessage);
|
expect(manager.getMessage()).toBe(errorMessage);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { MessageController } from '../../../src/components-lib/message/controller';
|
||||||
|
import { TROUBLESHOOTING_URL } from '../../../src/const';
|
||||||
|
import { Message, MessageType } from '../../../src/types';
|
||||||
|
|
||||||
|
describe('MessageController', () => {
|
||||||
|
describe('should return the correct message string', () => {
|
||||||
|
it('should return simple message string', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getMessageString(message)).toBe('Message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should embed simple string context', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
context: 'Context',
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getMessageString(message)).toBe('Message: Context');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return the correct icon', () => {
|
||||||
|
describe('when icon is specified', () => {
|
||||||
|
it.each([['info' as const], ['error' as const], ['connection' as const]])(
|
||||||
|
'%s',
|
||||||
|
(type: MessageType) => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
icon: 'mdi:car',
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
expect(controller.getIcon(message)).toBe('mdi:car');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when type is an error', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
type: 'error',
|
||||||
|
};
|
||||||
|
expect(controller.getIcon(message)).toBe('mdi:alert-circle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('by default', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
};
|
||||||
|
expect(controller.getIcon(message)).toBe('mdi:information-outline');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should show troubleshooting link', () => {
|
||||||
|
it('should show for errors', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = { message: 'Error message', type: 'error' };
|
||||||
|
expect(controller.shouldShowTroubleshootingURL(message)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should not show for other types', () => {
|
||||||
|
it.each([['info' as const], ['connection' as const]])(
|
||||||
|
'%s',
|
||||||
|
(type: MessageType) => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
icon: 'mdi:car',
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
expect(controller.shouldShowTroubleshootingURL(message)).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should show correct URL', () => {
|
||||||
|
it('by default', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = { message: 'Error message', type: 'error' };
|
||||||
|
expect(controller.getTroubleshootingURL(message)).toBe(TROUBLESHOOTING_URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when specified', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const troubleshootingURL = 'http://localhost/troubleshooting.md';
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Error message',
|
||||||
|
type: 'error',
|
||||||
|
troubleshootingURL: troubleshootingURL,
|
||||||
|
};
|
||||||
|
expect(controller.getTroubleshootingURL(message)).toBe(troubleshootingURL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get context strings', () => {
|
||||||
|
it('for no context', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getContextStrings(message)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for simple string', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
context: 'Context',
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getContextStrings(message)).toEqual(['Context']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for object', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const obj = { one: 1, two: 2 };
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
context: obj,
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getContextStrings(message)).toEqual([yaml.dump(obj)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for array', () => {
|
||||||
|
const controller = new MessageController();
|
||||||
|
const array = ['one', 'two'];
|
||||||
|
const message: Message = {
|
||||||
|
message: 'Message',
|
||||||
|
context: array,
|
||||||
|
type: 'info',
|
||||||
|
};
|
||||||
|
expect(controller.getContextStrings(message)).toEqual(
|
||||||
|
array.map((item) => yaml.dump(item)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { expect, it, vi } from 'vitest';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from '../../../src/components-lib/message/dispatch';
|
||||||
|
import { FrigateCardError } from '../../../src/types';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
it('should ignore non-error', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener('frigate-card:message', handler);
|
||||||
|
|
||||||
|
dispatchFrigateCardErrorEvent(element, 'NOT_FRIGATE_EVENT');
|
||||||
|
|
||||||
|
expect(handler).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch error', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener('frigate-card:message', handler);
|
||||||
|
|
||||||
|
dispatchFrigateCardErrorEvent(element, new Error('ERROR'));
|
||||||
|
|
||||||
|
expect(handler).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
message: 'ERROR',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch error with context', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener('frigate-card:message', handler);
|
||||||
|
|
||||||
|
dispatchFrigateCardErrorEvent(element, new FrigateCardError('ERROR', 'CONTEXT'));
|
||||||
|
|
||||||
|
expect(handler).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
detail: expect.objectContaining({
|
||||||
|
message: 'ERROR',
|
||||||
|
context: 'CONTEXT',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user