refactor: Refactor internal error / info message components (#1842)
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
FrigateCardError,
|
||||
MESSAGE_TYPE_PRIORITIES,
|
||||
Message,
|
||||
MessageType,
|
||||
} from '../types';
|
||||
import { FrigateCardError, Message, MessageType } from '../types';
|
||||
import { errorToConsole } from '../utils/basic';
|
||||
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 {
|
||||
protected _message: Message | null = null;
|
||||
protected _api: CardMessageAPI;
|
||||
@@ -52,17 +58,20 @@ export class MessageManager {
|
||||
|
||||
errorToConsole(error);
|
||||
this.setMessageIfHigherPriority({
|
||||
message: prefix ? `${prefix}: ${error.message}` : error.message,
|
||||
message: prefix ? `${prefix}: ${error.message}` : String(error.message),
|
||||
type: 'error',
|
||||
...(error instanceof FrigateCardError && { context: error.context }),
|
||||
});
|
||||
}
|
||||
|
||||
public setMessageIfHigherPriority(message: Message): boolean {
|
||||
const resolveMessageType = (message: Message): MessageType => {
|
||||
return message.type ?? 'info';
|
||||
};
|
||||
const currentPriority = this._message
|
||||
? MESSAGE_TYPE_PRIORITIES[this._message.type]
|
||||
? MESSAGE_TYPE_PRIORITIES[resolveMessageType(this._message)]
|
||||
: 0;
|
||||
const newPriority = MESSAGE_TYPE_PRIORITIES[message.type];
|
||||
const newPriority = MESSAGE_TYPE_PRIORITIES[resolveMessageType(message)];
|
||||
|
||||
if (this._message && newPriority < currentPriority) {
|
||||
return false;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LitElement, ReactiveController } from 'lit';
|
||||
import { FrigateCardMessageEventTarget } from '../../components/message.js';
|
||||
import { MediaLoadedInfo } from '../../types.js';
|
||||
import {
|
||||
FrigateCardMediaLoadedEventTarget,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
} from '../../utils/media-info.js';
|
||||
import { FrigateCardMessageEventTarget } from '../message/dispatch.js';
|
||||
|
||||
interface LiveViewContext {
|
||||
// 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 {
|
||||
return renderMessage({
|
||||
message: localize('error.diagnostics'),
|
||||
type: 'diagnostics',
|
||||
icon: 'mdi:information',
|
||||
icon: 'mdi:cogs',
|
||||
context: this._diagnostics,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ConditionsManagerEpoch,
|
||||
evaluateConditionViaEvent,
|
||||
} from '../card-controller/conditions-manager.js';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||
import {
|
||||
FrigateConditional,
|
||||
MenuIcon,
|
||||
@@ -29,7 +30,6 @@ import { localize } from '../localize/localize.js';
|
||||
import elementsStyle from '../scss/elements.scss';
|
||||
import { FrigateCardError } from '../types.js';
|
||||
import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js';
|
||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
|
||||
/* 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 { MediaQueriesResults } from '../view/media-queries-results';
|
||||
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 './thumbnail.js';
|
||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { MicrophoneState } from '../../card-controller/types.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.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 { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import {
|
||||
@@ -42,7 +43,6 @@ import { getTextDirection } from '../../utils/text-direction.js';
|
||||
import { View } from '../../view/view.js';
|
||||
import '../carousel';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { dispatchFrigateCardErrorEvent } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { FrigateCardPTZ } from '../ptz.js';
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
dispatchMediaPlayEvent,
|
||||
} from '../../../utils/media-info.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.
|
||||
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
|
||||
@@ -30,7 +30,10 @@ import {
|
||||
} from '../../../utils/media.js';
|
||||
import { screenshotMedia } from '../../../utils/screenshot.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';
|
||||
|
||||
// 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 { 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 { TROUBLESHOOTING_URL } from '../const.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { MessageController } from '../components-lib/message/controller.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import messageStyle from '../scss/message.scss';
|
||||
import { FrigateCardError, Message, MessageType } from '../types.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { Message } from '../types.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')
|
||||
export class FrigateCardMessage extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public message: string | TemplateResult<1> = '';
|
||||
public message?: Message;
|
||||
|
||||
@property({ attribute: false })
|
||||
public context?: unknown;
|
||||
private _controller = new MessageController();
|
||||
|
||||
@property({ attribute: false })
|
||||
public icon?: string;
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public dotdotdot?: boolean;
|
||||
const messageTemplate = html`
|
||||
${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.
|
||||
protected render(): TemplateResult {
|
||||
const icon = this.icon ? this.icon : 'mdi:information-outline';
|
||||
const icon = this._controller.getIcon(this.message);
|
||||
const classes = {
|
||||
dotdotdot: !!this.dotdotdot,
|
||||
};
|
||||
|
||||
const renderContext = (contextItem: unknown): TemplateResult => {
|
||||
return html`<pre>${yaml.dump(contextItem)}</pre>`;
|
||||
dotdotdot: !!this.message?.dotdotdot,
|
||||
};
|
||||
|
||||
return html` <div class="wrapper">
|
||||
@@ -42,18 +42,10 @@ export class FrigateCardMessage extends LitElement {
|
||||
<frigate-card-icon .icon="${{ icon: icon }}"></frigate-card-icon>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<span class="${classMap(classes)}">
|
||||
${this.message
|
||||
? html`${this.message}${this.context && typeof this.context === 'string'
|
||||
? ': ' + this.context
|
||||
: ''}`
|
||||
: ''}
|
||||
</span>
|
||||
${this.context && Array.isArray(this.context)
|
||||
? this.context.map((contextItem) => renderContext(contextItem))
|
||||
: typeof this.context === 'object'
|
||||
? renderContext(this.context)
|
||||
: ''}
|
||||
<span class="${classMap(classes)}">${messageTemplate}</span>
|
||||
${this._controller
|
||||
.getContextStrings(this.message)
|
||||
.map((contextItem) => html`<pre>${contextItem}</pre>`)}
|
||||
</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 {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
|
||||
'frigate-card-error-message': FrigateCardErrorMessage;
|
||||
'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 { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.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 { ViewMediaClassifier } from '../view/media-classifier.js';
|
||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
|
||||
// The minimum width of a thumbnail with details enabled.
|
||||
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 { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
||||
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
||||
import { renderProgressIndicator } from '../message.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
|
||||
@customElement('frigate-card-viewer-provider')
|
||||
export class FrigateCardViewerProvider
|
||||
|
||||
+3
-13
@@ -45,23 +45,13 @@ export interface MediaLoadedInfo {
|
||||
|
||||
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 {
|
||||
message: unknown;
|
||||
type: MessageType;
|
||||
message: string;
|
||||
type?: MessageType;
|
||||
icon?: string;
|
||||
context?: unknown;
|
||||
dotdotdot?: boolean;
|
||||
troubleshootingURL?: string;
|
||||
}
|
||||
|
||||
export interface FrigateCardMediaPlayer {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { html, TemplateResult } from 'lit';
|
||||
import { renderProgressIndicator } from '../components/message';
|
||||
import { renderProgressIndicator } from '../components/progress-indicator';
|
||||
import { CardWideConfig } from '../config/types';
|
||||
import { errorToConsole } from './basic';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createCardAPI } from '../test-utils';
|
||||
const createMessage = (options?: Partial<Message>): Message => {
|
||||
return {
|
||||
message: options?.message ?? 'message',
|
||||
type: options?.type ?? 'info',
|
||||
...(!!options?.type && { type: options.type }),
|
||||
...(!!options?.icon && { icon: options.icon }),
|
||||
...(!!options?.context && { context: options.context }),
|
||||
...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }),
|
||||
@@ -97,8 +97,11 @@ describe('MessageManager', () => {
|
||||
const errorMessage = createMessage({ type: 'error' });
|
||||
manager.setMessageIfHigherPriority(errorMessage);
|
||||
|
||||
const infoMessage = createMessage({ type: 'info' });
|
||||
manager.setMessageIfHigherPriority(infoMessage);
|
||||
const explicitInfoMessage = createMessage({ type: 'info' });
|
||||
manager.setMessageIfHigherPriority(explicitInfoMessage);
|
||||
|
||||
const implicitInfoMessage = createMessage();
|
||||
manager.setMessageIfHigherPriority(implicitInfoMessage);
|
||||
|
||||
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