mirror of https://github.com/OpenVidu/openvidu.git
openvidu-dashboard: one ranked sort state for the Rooms, Participants, Egress and Ingress lists
injectRankedSort() resolves the URL's sort and order with the fallback a wide range forces, and the sort fields of the ranked columns; rangeSpanMs() replaces the per-page span of the range.pull/924/head
parent
2461c21181
commit
e34946d44d
|
|
@ -1,7 +1,8 @@
|
|||
import { computed, inject, type Signal } from '@angular/core';
|
||||
import type { InfoLimits } from '@openvidu-dashboard/typings';
|
||||
import { type InfoLimits, SortOrder } from '@openvidu-dashboard/typings';
|
||||
|
||||
import { InfoService } from '../info.js';
|
||||
import { QueryStateService } from './query-state.js';
|
||||
|
||||
const HOUR_MS = 3_600_000;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
|
|
@ -61,3 +62,46 @@ export function injectRankedSortLimits(): Signal<InfoLimits | null> {
|
|||
|
||||
return computed(() => info.info()?.limits ?? null);
|
||||
}
|
||||
|
||||
/** The sort of a list whose ranked sorts a wide range rules out. */
|
||||
export interface RankedSort<S extends string> {
|
||||
/** The URL's sort, or the default sort while the range rules the URL's out. */
|
||||
readonly sort: Signal<S>;
|
||||
/** The URL's order, or descending with the default sort. */
|
||||
readonly order: Signal<SortOrder>;
|
||||
/** The sort fields of a ranked column: disabled, with the reason, while the range rules its sort out. */
|
||||
column(sort: S): { sortable: true; sortDisabledReason?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* The sort of a list from the URL's `sort` and `order`. Call it in an injection context.
|
||||
*
|
||||
* @param options.fallback The default sort, which every range allows.
|
||||
* @param options.ranked Column name of each ranked sort.
|
||||
* @param options.rangeMs Span of the list's range.
|
||||
* @param options.limitMs The ranked sorts' limit out of `GET /info`; `rankedSortMaxRangeMs` by default.
|
||||
*/
|
||||
export function injectRankedSort<S extends string>(options: {
|
||||
sorts: readonly S[];
|
||||
fallback: S;
|
||||
ranked: Readonly<Partial<Record<S, string>>>;
|
||||
rangeMs: Signal<number>;
|
||||
limitMs?: (limits: InfoLimits | null) => number | undefined;
|
||||
}): RankedSort<S> {
|
||||
const query = inject(QueryStateService);
|
||||
const limits = injectRankedSortLimits();
|
||||
const limitMs = options.limitMs ?? ((value: InfoLimits | null) => value?.rankedSortMaxRangeMs);
|
||||
const reasons = computed(() => rankedSortReasons(options.ranked, options.rangeMs(), limitMs(limits())));
|
||||
const urlSort = computed(() => query.oneOf('sort', options.sorts, options.fallback));
|
||||
const sort = computed(() => (reasons()[urlSort()] === undefined ? urlSort() : options.fallback));
|
||||
|
||||
return {
|
||||
sort,
|
||||
order: computed(() => (sort() === urlSort() ? query.order() : SortOrder.DESC)),
|
||||
column(key: S) {
|
||||
const reason = reasons()[key];
|
||||
|
||||
return reason === undefined ? { sortable: true } : { sortable: true, sortDisabledReason: reason };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isTimeExpression, isTimeZone, resolveRange, resolveTime } from './relative-time.js';
|
||||
import { isTimeExpression, isTimeZone, rangeSpanMs, resolveRange, resolveTime } from './relative-time.js';
|
||||
|
||||
const NOW = new Date('2026-10-13T12:40:00.000Z');
|
||||
|
||||
|
|
@ -65,4 +65,9 @@ describe('relative time', () => {
|
|||
expect(resolveRange({ from: 'now', to: 'now-1h', tz: 'UTC' }, NOW)).toBeUndefined();
|
||||
expect(resolveRange({ from: 'bogus', to: 'now', tz: 'UTC' }, NOW)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('spans a resolved range and is infinite otherwise', () => {
|
||||
expect(rangeSpanMs({ from: 'now-24h', to: 'now', tz: 'UTC' }, NOW)).toBe(86_400_000);
|
||||
expect(rangeSpanMs({ from: 'bogus', to: 'now', tz: 'UTC' }, NOW)).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,3 +69,10 @@ export function resolveRange(range: { from: string; to: string; tz: string }, no
|
|||
|
||||
return from !== undefined && to !== undefined && from < to ? { from, to } : undefined;
|
||||
}
|
||||
|
||||
/** Span of a range at `now` in milliseconds; infinite when the range does not resolve. */
|
||||
export function rangeSpanMs(range: { from: string; to: string; tz: string }, now: Date): number {
|
||||
const resolved = resolveRange(range, now);
|
||||
|
||||
return resolved === undefined ? Number.POSITIVE_INFINITY : resolved.to.getTime() - resolved.from.getTime();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import { injectTicker } from '../../core/refresh/ticker.js';
|
|||
import { EntityPreviewService } from '../../core/state/entity-preview.js';
|
||||
import { ListStateService } from '../../core/state/list-state.js';
|
||||
import { QueryStateService } from '../../core/state/query-state.js';
|
||||
import { injectRankedSortLimits, rankedSortReasons } from '../../core/state/ranked-sort.js';
|
||||
import { resolveRange } from '../../core/state/relative-time.js';
|
||||
import { injectRankedSort } from '../../core/state/ranked-sort.js';
|
||||
import { rangeSpanMs } from '../../core/state/relative-time.js';
|
||||
import { formatDuration, formatNumber, formatTimestamp } from '../../format/formatters.js';
|
||||
import { EGRESS_STATUS_LABELS, EGRESS_TYPE_LABELS, RECONCILER_ESTIMATE } from '../../format/labels.js';
|
||||
import { isRoomSid } from '../rooms/room-display.js';
|
||||
|
|
@ -96,7 +96,6 @@ export class EgressPage {
|
|||
private readonly listState = inject(ListStateService);
|
||||
private readonly previews = inject(EntityPreviewService);
|
||||
private readonly rangePicker = inject(RangePickerService);
|
||||
private readonly limits = injectRankedSortLimits();
|
||||
/** Last summary received, kept on the tiles when a summary request fails. */
|
||||
private lastSummary: EgressSummary | null = null;
|
||||
|
||||
|
|
@ -124,20 +123,15 @@ export class EgressPage {
|
|||
|
||||
return value !== null && isRoomSid(value) ? value : null;
|
||||
});
|
||||
private readonly rangeMs = computed(() => {
|
||||
const resolved = resolveRange(this.listState.range(), new Date());
|
||||
|
||||
return resolved === undefined ? Number.POSITIVE_INFINITY : resolved.to.getTime() - resolved.from.getTime();
|
||||
private readonly rangeMs = computed(() => rangeSpanMs(this.listState.range(), new Date()));
|
||||
private readonly rankedSort = injectRankedSort({
|
||||
sorts: SORTS,
|
||||
fallback: EgressSort.STARTED_AT,
|
||||
ranked: RANKED_SORTS,
|
||||
rangeMs: this.rangeMs
|
||||
});
|
||||
private readonly sortReasons = computed(() =>
|
||||
rankedSortReasons(RANKED_SORTS, this.rangeMs(), this.limits()?.rankedSortMaxRangeMs)
|
||||
);
|
||||
private readonly urlSort = computed(() => this.query.oneOf('sort', SORTS, EgressSort.STARTED_AT));
|
||||
/** The URL's sort, or Started descending when the range is too wide for it. */
|
||||
protected readonly sort = computed<EgressSort>(() =>
|
||||
this.sortReasons()[this.urlSort()] === undefined ? this.urlSort() : EgressSort.STARTED_AT
|
||||
);
|
||||
protected readonly order = computed(() => (this.sort() === this.urlSort() ? this.query.order() : 'desc'));
|
||||
protected readonly sort = this.rankedSort.sort;
|
||||
protected readonly order = this.rankedSort.order;
|
||||
|
||||
private readonly request = computed<EgressListRequest>(() => ({
|
||||
range: this.listState.range(),
|
||||
|
|
@ -175,31 +169,23 @@ export class EgressPage {
|
|||
protected readonly rows = computed(() => this.page()?.items ?? []);
|
||||
protected readonly summary = computed(() => this.loader.data()?.summary ?? null);
|
||||
|
||||
protected readonly columns = computed<TableColumn[]>(() => {
|
||||
const reasons = this.sortReasons();
|
||||
const ranked = (sort: EgressSort) => ({
|
||||
protected readonly columns = computed<TableColumn[]>(() => [
|
||||
{ key: 'egressId', header: 'Egress ID', hideable: false, card: 'title' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
{ key: 'type', header: 'Type' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: EgressSort.STARTED_AT,
|
||||
header: 'Started',
|
||||
sortable: true,
|
||||
...(reasons[sort] === undefined ? {} : { sortDisabledReason: reasons[sort] })
|
||||
});
|
||||
|
||||
return [
|
||||
{ key: 'egressId', header: 'Egress ID', hideable: false, card: 'title' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
{ key: 'type', header: 'Type' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: EgressSort.STARTED_AT,
|
||||
header: 'Started',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{ key: EgressSort.ENDED_AT, header: 'Ended', headerHint: this.tz(), ...ranked('endedAt') },
|
||||
{ key: EgressSort.DURATION, header: 'Duration', align: 'end', ...ranked('duration') },
|
||||
{ key: 'outputs', header: 'Outputs', card: 'metric' }
|
||||
];
|
||||
});
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{ key: EgressSort.ENDED_AT, header: 'Ended', headerHint: this.tz(), ...this.rankedSort.column('endedAt') },
|
||||
{ key: EgressSort.DURATION, header: 'Duration', align: 'end', ...this.rankedSort.column('duration') },
|
||||
{ key: 'outputs', header: 'Outputs', card: 'metric' }
|
||||
]);
|
||||
|
||||
protected readonly trackEgress = (item: EgressListItem): string => item.egressId;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import { injectListLoader } from '../../core/refresh/list-loader.js';
|
|||
import { EntityPreviewService } from '../../core/state/entity-preview.js';
|
||||
import { ListStateService } from '../../core/state/list-state.js';
|
||||
import { QueryStateService } from '../../core/state/query-state.js';
|
||||
import { injectRankedSortLimits, rankedSortReasons } from '../../core/state/ranked-sort.js';
|
||||
import { resolveRange } from '../../core/state/relative-time.js';
|
||||
import { injectRankedSort } from '../../core/state/ranked-sort.js';
|
||||
import { rangeSpanMs } from '../../core/state/relative-time.js';
|
||||
import { formatDuration, formatNumber, formatTimestamp } from '../../format/formatters.js';
|
||||
import { INFO_TEXTS } from '../../format/info-texts.js';
|
||||
import { INGRESS_INPUT_LABELS, INGRESS_STATUS_LABELS } from '../../format/labels.js';
|
||||
|
|
@ -90,7 +90,6 @@ export class IngressPage {
|
|||
private readonly listState = inject(ListStateService);
|
||||
private readonly previews = inject(EntityPreviewService);
|
||||
private readonly rangePicker = inject(RangePickerService);
|
||||
private readonly limits = injectRankedSortLimits();
|
||||
/** Last summary received, kept on the tiles when a summary request fails. */
|
||||
private lastSummary: IngressSummary | null = null;
|
||||
|
||||
|
|
@ -113,20 +112,15 @@ export class IngressPage {
|
|||
});
|
||||
protected readonly q = computed(() => listSearchQuery(this.query.q(), 'ingress'));
|
||||
protected readonly exactId = computed(() => exactListId(this.q(), 'ingress'));
|
||||
private readonly rangeMs = computed(() => {
|
||||
const resolved = resolveRange(this.listState.range(), new Date());
|
||||
|
||||
return resolved === undefined ? Number.POSITIVE_INFINITY : resolved.to.getTime() - resolved.from.getTime();
|
||||
private readonly rangeMs = computed(() => rangeSpanMs(this.listState.range(), new Date()));
|
||||
private readonly rankedSort = injectRankedSort({
|
||||
sorts: SORTS,
|
||||
fallback: IngressSort.CREATED_AT,
|
||||
ranked: METRIC_SORTS,
|
||||
rangeMs: this.rangeMs
|
||||
});
|
||||
private readonly sortReasons = computed(() =>
|
||||
rankedSortReasons(METRIC_SORTS, this.rangeMs(), this.limits()?.rankedSortMaxRangeMs)
|
||||
);
|
||||
private readonly urlSort = computed(() => this.query.oneOf('sort', SORTS, IngressSort.CREATED_AT));
|
||||
/** The URL's sort, or Created descending when the range is too wide for it. */
|
||||
protected readonly sort = computed<IngressSort>(() =>
|
||||
this.sortReasons()[this.urlSort()] === undefined ? this.urlSort() : IngressSort.CREATED_AT
|
||||
);
|
||||
protected readonly order = computed(() => (this.sort() === this.urlSort() ? this.query.order() : 'desc'));
|
||||
protected readonly sort = this.rankedSort.sort;
|
||||
protected readonly order = this.rankedSort.order;
|
||||
|
||||
private readonly request = computed<IngressListRequest>(() => ({
|
||||
range: this.listState.range(),
|
||||
|
|
@ -163,42 +157,40 @@ export class IngressPage {
|
|||
protected readonly rows = computed(() => this.page()?.items ?? []);
|
||||
protected readonly summary = computed(() => this.loader.data()?.summary ?? null);
|
||||
|
||||
protected readonly columns = computed<TableColumn[]>(() => {
|
||||
const reasons = this.sortReasons();
|
||||
const metric = (sort: IngressSort) => ({
|
||||
protected readonly columns = computed<TableColumn[]>(() => [
|
||||
{ key: 'ingressId', header: 'Ingress ID', hideable: false, card: 'title' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
{ key: 'input', header: 'Input' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: IngressSort.CREATED_AT,
|
||||
header: 'Created',
|
||||
sortable: true,
|
||||
...(reasons[sort] === undefined ? {} : { sortDisabledReason: reasons[sort] })
|
||||
});
|
||||
|
||||
return [
|
||||
{ key: 'ingressId', header: 'Ingress ID', hideable: false, card: 'title' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
{ key: 'input', header: 'Input' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: IngressSort.CREATED_AT,
|
||||
header: 'Created',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{
|
||||
key: IngressSort.TOTAL_DURATION,
|
||||
header: 'Total duration',
|
||||
align: 'end',
|
||||
...metric('totalDuration')
|
||||
},
|
||||
{ key: IngressSort.SESSIONS, header: 'Sessions', align: 'end', card: 'metric', ...metric('sessions') },
|
||||
{
|
||||
key: IngressSort.LAST_SESSION,
|
||||
header: 'Last session',
|
||||
headerHint: this.tz(),
|
||||
...metric('lastSession')
|
||||
}
|
||||
];
|
||||
});
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{
|
||||
key: IngressSort.TOTAL_DURATION,
|
||||
header: 'Total duration',
|
||||
align: 'end',
|
||||
...this.rankedSort.column('totalDuration')
|
||||
},
|
||||
{
|
||||
key: IngressSort.SESSIONS,
|
||||
header: 'Sessions',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
...this.rankedSort.column('sessions')
|
||||
},
|
||||
{
|
||||
key: IngressSort.LAST_SESSION,
|
||||
header: 'Last session',
|
||||
headerHint: this.tz(),
|
||||
...this.rankedSort.column('lastSession')
|
||||
}
|
||||
]);
|
||||
|
||||
protected readonly trackIngress = (item: IngressListItem): string => item.ingressId;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { exactListId, listSearchQuery } from '../../components/search/list-searc
|
|||
import { EntityPreviewService } from '../../core/state/entity-preview.js';
|
||||
import { ListStateService } from '../../core/state/list-state.js';
|
||||
import { QueryStateService } from '../../core/state/query-state.js';
|
||||
import { injectRankedSortLimits, rankedSortReasons } from '../../core/state/ranked-sort.js';
|
||||
import { resolveRange } from '../../core/state/relative-time.js';
|
||||
import { injectRankedSort } from '../../core/state/ranked-sort.js';
|
||||
import { rangeSpanMs } from '../../core/state/relative-time.js';
|
||||
import { formatTimestamp } from '../../format/formatters.js';
|
||||
import { isRoomSid } from '../rooms/room-display.js';
|
||||
import { participantPreview } from './participant-display.js';
|
||||
|
|
@ -34,7 +34,6 @@ export function injectParticipantSessions(options: { kindColumn: boolean }) {
|
|||
const listState = inject(ListStateService);
|
||||
const previews = inject(EntityPreviewService);
|
||||
const rangePicker = inject(RangePickerService);
|
||||
const limits = injectRankedSortLimits();
|
||||
|
||||
const q = computed(() => listSearchQuery(query.q(), 'participant'));
|
||||
const room = computed(() => {
|
||||
|
|
@ -42,24 +41,15 @@ export function injectParticipantSessions(options: { kindColumn: boolean }) {
|
|||
|
||||
return value !== null && isRoomSid(value) ? value : null;
|
||||
});
|
||||
const rangeMs = computed(() => {
|
||||
const resolved = resolveRange(listState.range(), new Date());
|
||||
|
||||
return resolved === undefined ? Number.POSITIVE_INFINITY : resolved.to.getTime() - resolved.from.getTime();
|
||||
const rangeMs = computed(() => rangeSpanMs(listState.range(), new Date()));
|
||||
const ranked = injectRankedSort({
|
||||
sorts: SORTS,
|
||||
fallback: ParticipantSort.JOINED_AT,
|
||||
ranked: RANKED_SORTS,
|
||||
rangeMs,
|
||||
// The ranked sorts apply up to the participants limit, or with any range inside one room.
|
||||
limitMs: (limits) => (room() === null ? limits?.participantRankedSortMaxRangeMs : undefined)
|
||||
});
|
||||
/** The ranked sorts apply up to the participants limit, or with any range inside one room. */
|
||||
const sortReasons = computed(() =>
|
||||
rankedSortReasons(
|
||||
RANKED_SORTS,
|
||||
rangeMs(),
|
||||
room() === null ? limits()?.participantRankedSortMaxRangeMs : undefined
|
||||
)
|
||||
);
|
||||
const urlSort = computed(() => query.oneOf('sort', SORTS, ParticipantSort.JOINED_AT));
|
||||
/** The URL's sort, or Joined descending when the range is too wide for it. */
|
||||
const sort = computed<ParticipantSort>(() =>
|
||||
sortReasons()[urlSort()] === undefined ? urlSort() : ParticipantSort.JOINED_AT
|
||||
);
|
||||
|
||||
return {
|
||||
q,
|
||||
|
|
@ -70,42 +60,34 @@ export function injectParticipantSessions(options: { kindColumn: boolean }) {
|
|||
return PARTICIPANT_STATUSES.includes(value as ParticipantStatus) ? (value as ParticipantStatus) : null;
|
||||
}),
|
||||
room,
|
||||
sort,
|
||||
order: computed(() => (sort() === urlSort() ? query.order() : 'desc')),
|
||||
columns: computed<TableColumn[]>(() => {
|
||||
const reasons = sortReasons();
|
||||
const ranked = (key: ParticipantSort) => ({
|
||||
sort: ranked.sort,
|
||||
order: ranked.order,
|
||||
columns: computed<TableColumn[]>(() => [
|
||||
{ key: 'sid', header: 'SID', hideable: false, card: 'title' },
|
||||
{ key: 'identity', header: 'Identity' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
...(options.kindColumn ? [{ key: 'kind', header: 'Kind' }] : []),
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: ParticipantSort.JOINED_AT,
|
||||
header: 'Joined',
|
||||
sortable: true,
|
||||
...(reasons[key] === undefined ? {} : { sortDisabledReason: reasons[key] })
|
||||
});
|
||||
|
||||
return [
|
||||
{ key: 'sid', header: 'SID', hideable: false, card: 'title' },
|
||||
{ key: 'identity', header: 'Identity' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'room', header: 'Room', card: 'metric' },
|
||||
...(options.kindColumn ? [{ key: 'kind', header: 'Kind' }] : []),
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: ParticipantSort.JOINED_AT,
|
||||
header: 'Joined',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: query.tz()
|
||||
},
|
||||
{ key: ParticipantSort.LEFT_AT, header: 'Left', headerHint: query.tz(), ...ranked('leftAt') },
|
||||
{
|
||||
key: ParticipantSort.DURATION,
|
||||
header: 'Duration',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
...ranked('duration')
|
||||
},
|
||||
{ key: 'published', header: 'Published', card: 'metric' },
|
||||
{ key: 'node', header: 'Node', defaultHidden: true }
|
||||
];
|
||||
}),
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: query.tz()
|
||||
},
|
||||
{ key: ParticipantSort.LEFT_AT, header: 'Left', headerHint: query.tz(), ...ranked.column('leftAt') },
|
||||
{
|
||||
key: ParticipantSort.DURATION,
|
||||
header: 'Duration',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
...ranked.column('duration')
|
||||
},
|
||||
{ key: 'published', header: 'Published', card: 'metric' },
|
||||
{ key: 'node', header: 'Node', defaultHidden: true }
|
||||
]),
|
||||
/** Dates as on the other lists: the time alone inside the current day of a range up to 24 h. */
|
||||
time: (value: string): string =>
|
||||
formatTimestamp(value, { tz: query.tz(), now: new Date(), rangeMs: rangeMs() }),
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import { injectTicker } from '../../core/refresh/ticker.js';
|
|||
import { EntityPreviewService } from '../../core/state/entity-preview.js';
|
||||
import { ListStateService } from '../../core/state/list-state.js';
|
||||
import { QueryStateService } from '../../core/state/query-state.js';
|
||||
import { injectRankedSortLimits, rankedSortReasons } from '../../core/state/ranked-sort.js';
|
||||
import { resolveRange } from '../../core/state/relative-time.js';
|
||||
import { injectRankedSort } from '../../core/state/ranked-sort.js';
|
||||
import { rangeSpanMs } from '../../core/state/relative-time.js';
|
||||
import { formatNumber, formatTimestamp } from '../../format/formatters.js';
|
||||
import { INFO_TEXTS } from '../../format/info-texts.js';
|
||||
import { RECONCILER_ESTIMATE } from '../../format/labels.js';
|
||||
|
|
@ -95,7 +95,6 @@ export class RoomsPage {
|
|||
private readonly listState = inject(ListStateService);
|
||||
private readonly previews = inject(EntityPreviewService);
|
||||
private readonly rangePicker = inject(RangePickerService);
|
||||
private readonly limits = injectRankedSortLimits();
|
||||
|
||||
protected readonly features = ROOM_FEATURES;
|
||||
protected readonly estimate = RECONCILER_ESTIMATE;
|
||||
|
|
@ -113,29 +112,21 @@ export class RoomsPage {
|
|||
protected readonly exactId = computed(() => exactListId(this.q(), 'room'));
|
||||
protected readonly selectedFeatures = computed(() => this.query.csv('features', FEATURE_KEYS));
|
||||
protected readonly nodeId = computed(() => (this.nodeFilter() ? this.query.value('nodeId') : null));
|
||||
private readonly rangeMs = computed(() => {
|
||||
const resolved = resolveRange(this.listState.range(), this.now());
|
||||
|
||||
return resolved === undefined ? Number.POSITIVE_INFINITY : resolved.to.getTime() - resolved.from.getTime();
|
||||
});
|
||||
private readonly rangeMs = computed(() => rangeSpanMs(this.listState.range(), this.now()));
|
||||
/** Range span when the range last changed, so the ticker never re-renders the headers. */
|
||||
private readonly requestRangeMs = computed(() => {
|
||||
this.listState.range();
|
||||
|
||||
return untracked(this.rangeMs);
|
||||
});
|
||||
private readonly sortReasons = computed(() =>
|
||||
rankedSortReasons(RANKED_SORTS, this.requestRangeMs(), this.limits()?.rankedSortMaxRangeMs)
|
||||
);
|
||||
/** The URL's sort, or Started descending when the range is too wide for it. */
|
||||
protected readonly sort = computed<RoomSort>(() => {
|
||||
const sort = this.query.oneOf('sort', SORTS, RoomSort.CREATED_AT);
|
||||
|
||||
return this.sortReasons()[sort] === undefined ? sort : RoomSort.CREATED_AT;
|
||||
private readonly rankedSort = injectRankedSort({
|
||||
sorts: SORTS,
|
||||
fallback: RoomSort.CREATED_AT,
|
||||
ranked: RANKED_SORTS,
|
||||
rangeMs: this.requestRangeMs
|
||||
});
|
||||
protected readonly order = computed(() =>
|
||||
this.sort() === this.query.oneOf('sort', SORTS, RoomSort.CREATED_AT) ? this.query.order() : 'desc'
|
||||
);
|
||||
protected readonly sort = this.rankedSort.sort;
|
||||
protected readonly order = this.rankedSort.order;
|
||||
|
||||
private readonly request = computed<RoomsListRequest>(() => ({
|
||||
range: this.listState.range(),
|
||||
|
|
@ -176,54 +167,46 @@ export class RoomsPage {
|
|||
protected readonly page = computed(() => this.loader.data()?.rooms ?? null);
|
||||
protected readonly summary = computed(() => this.loader.data()?.summary ?? null);
|
||||
|
||||
protected readonly columns = computed<TableColumn[]>(() => {
|
||||
const reasons = this.sortReasons();
|
||||
const ranked = (sort: RoomSort) => ({
|
||||
protected readonly columns = computed<TableColumn[]>(() => [
|
||||
{ key: 'sid', header: 'SID', hideable: false, card: 'title' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: RoomSort.CREATED_AT,
|
||||
header: 'Started',
|
||||
sortable: true,
|
||||
...(reasons[sort] === undefined ? {} : { sortDisabledReason: reasons[sort] })
|
||||
});
|
||||
|
||||
return [
|
||||
{ key: 'sid', header: 'SID', hideable: false, card: 'title' },
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'status', header: 'Status', hideable: false, card: 'status' },
|
||||
{
|
||||
key: RoomSort.CREATED_AT,
|
||||
header: 'Started',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{ key: RoomSort.ENDED_AT, header: 'Ended', headerHint: this.tz(), ...ranked('endedAt') },
|
||||
{
|
||||
key: RoomSort.DURATION,
|
||||
header: 'Duration',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
...ranked('duration')
|
||||
},
|
||||
{
|
||||
key: RoomSort.UNIQUE_IDENTITIES,
|
||||
header: 'Unique identities',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
info: INFO_TEXTS.uniqueIdentities,
|
||||
...ranked('uniqueIdentities')
|
||||
},
|
||||
{
|
||||
key: RoomSort.ACTIVE,
|
||||
header: 'Active',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
info: INFO_TEXTS.activeParticipants,
|
||||
...ranked('active')
|
||||
},
|
||||
{ key: 'bytes', header: 'Data up / down', align: 'end' },
|
||||
{ key: 'features', header: 'Features', info: INFO_TEXTS.roomFeatures },
|
||||
{ key: 'node', header: 'Node', defaultHidden: true }
|
||||
];
|
||||
});
|
||||
hideable: false,
|
||||
card: 'timestamp',
|
||||
headerHint: this.tz()
|
||||
},
|
||||
{ key: RoomSort.ENDED_AT, header: 'Ended', headerHint: this.tz(), ...this.rankedSort.column('endedAt') },
|
||||
{
|
||||
key: RoomSort.DURATION,
|
||||
header: 'Duration',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
...this.rankedSort.column('duration')
|
||||
},
|
||||
{
|
||||
key: RoomSort.UNIQUE_IDENTITIES,
|
||||
header: 'Unique identities',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
info: INFO_TEXTS.uniqueIdentities,
|
||||
...this.rankedSort.column('uniqueIdentities')
|
||||
},
|
||||
{
|
||||
key: RoomSort.ACTIVE,
|
||||
header: 'Active',
|
||||
align: 'end',
|
||||
card: 'metric',
|
||||
info: INFO_TEXTS.activeParticipants,
|
||||
...this.rankedSort.column('active')
|
||||
},
|
||||
{ key: 'bytes', header: 'Data up / down', align: 'end' },
|
||||
{ key: 'features', header: 'Features', info: INFO_TEXTS.roomFeatures },
|
||||
{ key: 'node', header: 'Node', defaultHidden: true }
|
||||
]);
|
||||
|
||||
protected readonly trackRoom = (room: RoomListItem): string => room.sid;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue