@ethlete/components 1.0.0-next.34 → 1.0.0-next.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/fesm2022/ethlete-components.mjs +1324 -122
- package/fesm2022/ethlete-components.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-components.d.ts +498 -48
|
@@ -9,7 +9,7 @@ import { takeUntilDestroyed, toObservable, toSignal, outputFromObservable, outpu
|
|
|
9
9
|
import { Subject, take, tap, timer, takeUntil, fromEvent, filter, combineLatest, switchMap, map, distinctUntilChanged, EMPTY, debounceTime, merge, defer, finalize, of, isObservable, from, catchError, concat, scan, interval, animationFrameScheduler, EmptyError, exhaustMap, Observable, shareReplay, defaultIfEmpty, expand, toArray } from 'rxjs';
|
|
10
10
|
import * as i1$1 from '@angular/router';
|
|
11
11
|
import { Router, RouterLink, RouterLinkActive, NavigationEnd } from '@angular/router';
|
|
12
|
-
import { withArgs, queryStateSignal, isQueryStateSuccess, isQueryStateFailure, isQueryStateLoading, queryComputed, queryDevtoolsEntries, createQueryErrorResponse, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleEn, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToTitleDe, queryErrorMessages, extractQuery } from '@ethlete/query';
|
|
12
|
+
import { withArgs, queryStateSignal, isQueryStateSuccess, isQueryStateFailure, isQueryStateLoading, queryComputed, queryDevtoolsEntries, queryDevtoolsFaults, EMPTY_QUERY_DEVTOOLS_FAULT, isQueryDevtoolsFaultArmed, QUERY_DEVTOOLS_FAULT_STATUSES, clearQueryDevtoolsFaults, createQueryErrorResponse, setQueryDevtoolsFault, sumQueryDevtoolsStats, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleEn, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToTitleDe, queryErrorMessages, extractQuery } from '@ethlete/query';
|
|
13
13
|
import { createManagedMetadataKey, metadata, validate, FORM_FIELD, form } from '@angular/forms/signals';
|
|
14
14
|
import { tap as tap$1 } from 'rxjs/operators';
|
|
15
15
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
@@ -27316,12 +27316,14 @@ class InputDirective extends TextFieldControlDirective {
|
|
|
27316
27316
|
* split in its own `(input)` handler), it suppresses ours so the two can't clobber each other.
|
|
27317
27317
|
*/
|
|
27318
27318
|
this.nativeSyncSuppressed = false;
|
|
27319
|
-
|
|
27319
|
+
// A bound field may hold `null` for "empty" (a nullable string field, e.g. the query form's
|
|
27320
|
+
// search field), so neither of these may assume a string is there.
|
|
27321
|
+
this.hasValue = computed(() => this.mixed() || !!this.value(), /* @ts-ignore */
|
|
27320
27322
|
...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
|
|
27321
27323
|
this.controlType = signal(FORM_FIELD_CONTROL_TYPES.TEXT_INPUT, /* @ts-ignore */
|
|
27322
27324
|
...(ngDevMode ? [{ debugName: "controlType" }] : /* istanbul ignore next */ []));
|
|
27323
27325
|
/** The text the native input renders - empty while mixed so the raw value never reaches the DOM. */
|
|
27324
|
-
this.displayValue = computed(() => (this.mixed() ? '' : this.value()), /* @ts-ignore */
|
|
27326
|
+
this.displayValue = computed(() => (this.mixed() ? '' : (this.value() ?? '')), /* @ts-ignore */
|
|
27325
27327
|
...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
27326
27328
|
/** The placeholder the native input renders - `mixedLabel` overrides the consumer placeholder while mixed. */
|
|
27327
27329
|
this.effectivePlaceholder = computed(() => (this.mixed() ? this.resolvedMixedLabel() : this.placeholder()), /* @ts-ignore */
|
|
@@ -30519,7 +30521,7 @@ const RICH_TEXT_EDITOR_TOKEN_CODEC = new InjectionToken('RICH_TEXT_EDITOR_TOKEN_
|
|
|
30519
30521
|
const COALESCE_WINDOW_MS = 500;
|
|
30520
30522
|
/** How many states are kept. The oldest fall off the bottom - enough depth to be useful, bounded
|
|
30521
30523
|
* so a long editing session can't grow the stack without limit. */
|
|
30522
|
-
const MAX_ENTRIES = 100;
|
|
30524
|
+
const MAX_ENTRIES$1 = 100;
|
|
30523
30525
|
/**
|
|
30524
30526
|
* The characters `next` inserted over `prev` - a prefix/suffix diff, which is all that is needed to
|
|
30525
30527
|
* tell "typed a word character" from "typed a space". A deletion yields `''`.
|
|
@@ -30603,7 +30605,7 @@ const createRichTextEditorHistory = () => {
|
|
|
30603
30605
|
return;
|
|
30604
30606
|
}
|
|
30605
30607
|
entries.push({ value, selection });
|
|
30606
|
-
if (entries.length > MAX_ENTRIES)
|
|
30608
|
+
if (entries.length > MAX_ENTRIES$1)
|
|
30607
30609
|
entries.shift();
|
|
30608
30610
|
index = entries.length - 1;
|
|
30609
30611
|
syncFlags();
|
|
@@ -47009,7 +47011,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
47009
47011
|
}], ctorParameters: () => [], propDecorators: { urlForPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "etPaginationSeo", required: false }] }], pageTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageTitle", required: false }] }] } });
|
|
47010
47012
|
|
|
47011
47013
|
/** How long the copy button stays ticked after a successful write. */
|
|
47012
|
-
const COPIED_RESET_MS = 1200;
|
|
47014
|
+
const COPIED_RESET_MS$1 = 1200;
|
|
47015
|
+
/**
|
|
47016
|
+
* Containers with more entries than this are not rendered directly - their children are grouped into
|
|
47017
|
+
* collapsed slices of this size, so a 5000-item array costs 50 rows instead of 5000.
|
|
47018
|
+
*/
|
|
47019
|
+
const CHUNK_SIZE = 100;
|
|
47013
47020
|
const kindOf = (value) => {
|
|
47014
47021
|
if (value === null)
|
|
47015
47022
|
return 'null';
|
|
@@ -47019,10 +47026,26 @@ const kindOf = (value) => {
|
|
|
47019
47026
|
return 'array';
|
|
47020
47027
|
return typeof value;
|
|
47021
47028
|
};
|
|
47029
|
+
const entriesOf = (value) => {
|
|
47030
|
+
if (Array.isArray(value))
|
|
47031
|
+
return value.map((v, i) => ({ k: String(i), v }));
|
|
47032
|
+
if (value && typeof value === 'object') {
|
|
47033
|
+
return Object.entries(value).map(([k, v]) => ({ k, v }));
|
|
47034
|
+
}
|
|
47035
|
+
return [];
|
|
47036
|
+
};
|
|
47037
|
+
/** Keeps the number of sibling slices at or below `CHUNK_SIZE` by folding recursively for huge counts. */
|
|
47038
|
+
const chunkSizeFor = (count) => {
|
|
47039
|
+
let size = CHUNK_SIZE;
|
|
47040
|
+
while (count / size > CHUNK_SIZE)
|
|
47041
|
+
size *= CHUNK_SIZE;
|
|
47042
|
+
return size;
|
|
47043
|
+
};
|
|
47022
47044
|
/**
|
|
47023
47045
|
* A recursive, collapsible, searchable JSON tree used by the query devtools value explorer. Renders
|
|
47024
47046
|
* the *transformed* value the app actually sees (post `transformResponse`). Self-recurses via its
|
|
47025
|
-
* own selector
|
|
47047
|
+
* own selector - both for child entries and for the folded slices of an oversized container, which
|
|
47048
|
+
* re-enter as the same component with a `chunk` window over the same value.
|
|
47026
47049
|
*/
|
|
47027
47050
|
class QueryDevtoolsJsonComponent {
|
|
47028
47051
|
constructor() {
|
|
@@ -47034,6 +47057,12 @@ class QueryDevtoolsJsonComponent {
|
|
|
47034
47057
|
/** Lowercased search term; when set, the tree auto-expands and matches are highlighted. */
|
|
47035
47058
|
this.search = input('', /* @ts-ignore */
|
|
47036
47059
|
...(ngDevMode ? [{ debugName: "search" }] : /* istanbul ignore next */ []));
|
|
47060
|
+
/**
|
|
47061
|
+
* When set, this node is not the value itself but a folded window over its entries. It shares the
|
|
47062
|
+
* container's `path` so a child's persisted expansion does not depend on where the slice borders land.
|
|
47063
|
+
*/
|
|
47064
|
+
this.chunk = input(null, /* @ts-ignore */
|
|
47065
|
+
...(ngDevMode ? [{ debugName: "chunk" }] : /* istanbul ignore next */ []));
|
|
47037
47066
|
/** Full path of this node from the explorer root, used as the key for persisted expansion. */
|
|
47038
47067
|
this.path = input('', /* @ts-ignore */
|
|
47039
47068
|
...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
@@ -47050,66 +47079,80 @@ class QueryDevtoolsJsonComponent {
|
|
|
47050
47079
|
...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
|
|
47051
47080
|
this.isContainer = computed(() => this.kind() === 'array' || this.kind() === 'object', /* @ts-ignore */
|
|
47052
47081
|
...(ngDevMode ? [{ debugName: "isContainer" }] : /* istanbul ignore next */ []));
|
|
47053
|
-
|
|
47054
|
-
|
|
47055
|
-
|
|
47056
|
-
|
|
47057
|
-
|
|
47058
|
-
return Object.entries(value).map(([k, v]) => ({ k, v }));
|
|
47059
|
-
}
|
|
47060
|
-
return [];
|
|
47082
|
+
/** The entries this node covers: the container's own, or just the window a chunk stands for. */
|
|
47083
|
+
this.ownEntries = computed(() => {
|
|
47084
|
+
const entries = entriesOf(this.value());
|
|
47085
|
+
const chunk = this.chunk();
|
|
47086
|
+
return chunk ? entries.slice(chunk.start, chunk.end) : entries;
|
|
47061
47087
|
}, /* @ts-ignore */
|
|
47062
|
-
...(ngDevMode ? [{ debugName: "
|
|
47063
|
-
this.
|
|
47088
|
+
...(ngDevMode ? [{ debugName: "ownEntries" }] : /* istanbul ignore next */ []));
|
|
47089
|
+
this.childChunks = computed(() => {
|
|
47090
|
+
const entries = this.ownEntries();
|
|
47091
|
+
if (entries.length <= CHUNK_SIZE)
|
|
47092
|
+
return [];
|
|
47093
|
+
return buildChunks(entries, { offset: this.chunk()?.start ?? 0, isArray: this.kind() === 'array' });
|
|
47094
|
+
}, /* @ts-ignore */
|
|
47095
|
+
...(ngDevMode ? [{ debugName: "childChunks" }] : /* istanbul ignore next */ []));
|
|
47096
|
+
/** Entries rendered as rows - empty while they are folded into slices instead. */
|
|
47097
|
+
this.visibleEntries = computed(() => (this.childChunks().length ? [] : this.ownEntries()), /* @ts-ignore */
|
|
47098
|
+
...(ngDevMode ? [{ debugName: "visibleEntries" }] : /* istanbul ignore next */ []));
|
|
47099
|
+
/** Slices carry their window in the key so siblings persist independently of the container. */
|
|
47100
|
+
this.togglePath = computed(() => {
|
|
47101
|
+
const chunk = this.chunk();
|
|
47102
|
+
return chunk ? `${this.path()}#${chunk.start}` : this.path();
|
|
47103
|
+
}, /* @ts-ignore */
|
|
47104
|
+
...(ngDevMode ? [{ debugName: "togglePath" }] : /* istanbul ignore next */ []));
|
|
47105
|
+
this.defaultExpanded = computed(() => !this.chunk() && this.depth() < 1, /* @ts-ignore */
|
|
47106
|
+
...(ngDevMode ? [{ debugName: "defaultExpanded" }] : /* istanbul ignore next */ []));
|
|
47107
|
+
this.localExpanded = linkedSignal(() => this.defaultExpanded(), /* @ts-ignore */
|
|
47064
47108
|
...(ngDevMode ? [{ debugName: "localExpanded" }] : /* istanbul ignore next */ []));
|
|
47065
47109
|
this.expanded = computed(() => {
|
|
47066
47110
|
// Externally-persisted mode: resolve from the expanded/collapsed path sets, else the depth default.
|
|
47067
47111
|
if (this.toggleFn()) {
|
|
47068
|
-
const path = this.
|
|
47112
|
+
const path = this.togglePath();
|
|
47069
47113
|
if (this.collapsedPaths()?.has(path))
|
|
47070
47114
|
return false;
|
|
47071
47115
|
if (this.expandedPaths()?.has(path))
|
|
47072
47116
|
return true;
|
|
47073
|
-
return this.
|
|
47117
|
+
return this.defaultExpanded();
|
|
47074
47118
|
}
|
|
47075
47119
|
return this.localExpanded();
|
|
47076
47120
|
}, /* @ts-ignore */
|
|
47077
47121
|
...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
47078
|
-
|
|
47122
|
+
/** Only slices that actually contain a match unfold while searching, so a filter stays cheap. */
|
|
47123
|
+
this.chunkHasHit = computed(() => {
|
|
47124
|
+
const term = this.search();
|
|
47125
|
+
return !!term && this.ownEntries().some(({ k, v }) => k.toLowerCase().includes(term) || matchesDeep(v, term));
|
|
47126
|
+
}, /* @ts-ignore */
|
|
47127
|
+
...(ngDevMode ? [{ debugName: "chunkHasHit" }] : /* istanbul ignore next */ []));
|
|
47128
|
+
this.effectiveExpanded = computed(() => {
|
|
47129
|
+
if (!this.search())
|
|
47130
|
+
return this.expanded();
|
|
47131
|
+
return this.chunk() ? this.chunkHasHit() : true;
|
|
47132
|
+
}, /* @ts-ignore */
|
|
47079
47133
|
...(ngDevMode ? [{ debugName: "effectiveExpanded" }] : /* istanbul ignore next */ []));
|
|
47080
47134
|
this.copied = signal(false, /* @ts-ignore */
|
|
47081
47135
|
...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
47082
47136
|
this.copiedReset$ = new Subject();
|
|
47083
47137
|
this.copyLabel = computed(() => {
|
|
47084
|
-
const
|
|
47085
|
-
if (
|
|
47086
|
-
return `Copy
|
|
47087
|
-
if (this.kind() === '
|
|
47088
|
-
|
|
47138
|
+
const count = this.ownEntries().length;
|
|
47139
|
+
if (this.chunk())
|
|
47140
|
+
return `Copy slice (${count} ${count === 1 ? 'entry' : 'entries'})`;
|
|
47141
|
+
if (this.kind() === 'array')
|
|
47142
|
+
return `Copy array (${count} ${count === 1 ? 'item' : 'items'})`;
|
|
47143
|
+
if (this.kind() === 'object')
|
|
47089
47144
|
return `Copy object (${count} ${count === 1 ? 'key' : 'keys'})`;
|
|
47090
|
-
}
|
|
47091
47145
|
return 'Copy value';
|
|
47092
47146
|
}, /* @ts-ignore */
|
|
47093
47147
|
...(ngDevMode ? [{ debugName: "copyLabel" }] : /* istanbul ignore next */ []));
|
|
47094
47148
|
this.preview = computed(() => {
|
|
47095
|
-
const
|
|
47096
|
-
if (
|
|
47097
|
-
return
|
|
47098
|
-
const count = value ? Object.keys(value).length : 0;
|
|
47149
|
+
const count = this.ownEntries().length;
|
|
47150
|
+
if (this.kind() === 'array')
|
|
47151
|
+
return count ? `[…] ${count}` : '[]';
|
|
47099
47152
|
return count ? `{…} ${count}` : '{}';
|
|
47100
47153
|
}, /* @ts-ignore */
|
|
47101
47154
|
...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
47102
|
-
this.display = computed(() =>
|
|
47103
|
-
const value = this.value();
|
|
47104
|
-
const kind = this.kind();
|
|
47105
|
-
if (kind === 'string')
|
|
47106
|
-
return `"${value}"`;
|
|
47107
|
-
if (kind === 'null')
|
|
47108
|
-
return 'null';
|
|
47109
|
-
if (kind === 'undefined')
|
|
47110
|
-
return 'undefined';
|
|
47111
|
-
return String(value);
|
|
47112
|
-
}, /* @ts-ignore */
|
|
47155
|
+
this.display = computed(() => displayOf(this.value()), /* @ts-ignore */
|
|
47113
47156
|
...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
|
|
47114
47157
|
this.keyHit = computed(() => {
|
|
47115
47158
|
const term = this.search();
|
|
@@ -47124,7 +47167,7 @@ class QueryDevtoolsJsonComponent {
|
|
|
47124
47167
|
...(ngDevMode ? [{ debugName: "valueHit" }] : /* istanbul ignore next */ []));
|
|
47125
47168
|
// Each copy restarts the tick countdown; switchMap drops the pending reset of the previous one.
|
|
47126
47169
|
this.copiedReset$
|
|
47127
|
-
.pipe(switchMap(() => timer(COPIED_RESET_MS)), tap(() => this.copied.set(false)), takeUntilDestroyed())
|
|
47170
|
+
.pipe(switchMap(() => timer(COPIED_RESET_MS$1)), tap(() => this.copied.set(false)), takeUntilDestroyed())
|
|
47128
47171
|
.subscribe();
|
|
47129
47172
|
}
|
|
47130
47173
|
childPath(key) {
|
|
@@ -47134,18 +47177,19 @@ class QueryDevtoolsJsonComponent {
|
|
|
47134
47177
|
toggle() {
|
|
47135
47178
|
const toggleFn = this.toggleFn();
|
|
47136
47179
|
if (toggleFn) {
|
|
47137
|
-
toggleFn(this.
|
|
47180
|
+
toggleFn(this.togglePath(), !this.expanded());
|
|
47138
47181
|
return;
|
|
47139
47182
|
}
|
|
47140
47183
|
this.localExpanded.set(!this.localExpanded());
|
|
47141
47184
|
}
|
|
47142
47185
|
/**
|
|
47143
|
-
* Containers copy their whole subtree as JSON
|
|
47144
|
-
* without the display quotes, so an id or url can go straight
|
|
47186
|
+
* Containers copy their whole subtree as JSON, slices only the entries they cover; leaves copy
|
|
47187
|
+
* something pasteable - a raw string without the display quotes, so an id or url can go straight
|
|
47188
|
+
* into a search box.
|
|
47145
47189
|
*/
|
|
47146
47190
|
copyValue() {
|
|
47147
|
-
const value = this.value();
|
|
47148
47191
|
const kind = this.kind();
|
|
47192
|
+
const value = this.chunk() ? this.chunkValue() : this.value();
|
|
47149
47193
|
let text;
|
|
47150
47194
|
try {
|
|
47151
47195
|
if (kind === 'string')
|
|
@@ -47164,12 +47208,18 @@ class QueryDevtoolsJsonComponent {
|
|
|
47164
47208
|
.then(() => this.flagCopied())
|
|
47165
47209
|
.catch(() => undefined);
|
|
47166
47210
|
}
|
|
47211
|
+
chunkValue() {
|
|
47212
|
+
const entries = this.ownEntries();
|
|
47213
|
+
if (this.kind() === 'array')
|
|
47214
|
+
return entries.map((entry) => entry.v);
|
|
47215
|
+
return Object.fromEntries(entries.map((entry) => [entry.k, entry.v]));
|
|
47216
|
+
}
|
|
47167
47217
|
flagCopied() {
|
|
47168
47218
|
this.copied.set(true);
|
|
47169
47219
|
this.copiedReset$.next();
|
|
47170
47220
|
}
|
|
47171
47221
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: QueryDevtoolsJsonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
47172
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: QueryDevtoolsJsonComponent, isStandalone: true, selector: "et-query-devtools-json", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, nodeKey: { classPropertyName: "nodeKey", publicName: "nodeKey", isSignal: true, isRequired: false, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: false, transformFunction: null }, expandedPaths: { classPropertyName: "expandedPaths", publicName: "expandedPaths", isSignal: true, isRequired: false, transformFunction: null }, collapsedPaths: { classPropertyName: "collapsedPaths", publicName: "collapsedPaths", isSignal: true, isRequired: false, transformFunction: null }, toggleFn: { classPropertyName: "toggleFn", publicName: "toggleFn", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
47222
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: QueryDevtoolsJsonComponent, isStandalone: true, selector: "et-query-devtools-json", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, nodeKey: { classPropertyName: "nodeKey", publicName: "nodeKey", isSignal: true, isRequired: false, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, search: { classPropertyName: "search", publicName: "search", isSignal: true, isRequired: false, transformFunction: null }, chunk: { classPropertyName: "chunk", publicName: "chunk", isSignal: true, isRequired: false, transformFunction: null }, path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: false, transformFunction: null }, expandedPaths: { classPropertyName: "expandedPaths", publicName: "expandedPaths", isSignal: true, isRequired: false, transformFunction: null }, collapsedPaths: { classPropertyName: "collapsedPaths", publicName: "collapsedPaths", isSignal: true, isRequired: false, transformFunction: null }, toggleFn: { classPropertyName: "toggleFn", publicName: "toggleFn", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
47173
47223
|
@if (isContainer()) {
|
|
47174
47224
|
<div class="et-query-devtools-json-node">
|
|
47175
47225
|
<div class="et-query-devtools-json-row">
|
|
@@ -47180,7 +47230,9 @@ class QueryDevtoolsJsonComponent {
|
|
|
47180
47230
|
type="button"
|
|
47181
47231
|
>
|
|
47182
47232
|
<span class="et-query-devtools-json-caret">{{ effectiveExpanded() ? '▾' : '▸' }}</span>
|
|
47183
|
-
@if (
|
|
47233
|
+
@if (chunk(); as chunk) {
|
|
47234
|
+
<span class="et-query-devtools-json-chunk">{{ chunk.label }}</span>
|
|
47235
|
+
} @else if (nodeKey() !== null) {
|
|
47184
47236
|
<span [class.et-query-devtools-json-hit]="keyHit()" class="et-query-devtools-json-key">{{
|
|
47185
47237
|
nodeKey()
|
|
47186
47238
|
}}</span>
|
|
@@ -47202,7 +47254,19 @@ class QueryDevtoolsJsonComponent {
|
|
|
47202
47254
|
|
|
47203
47255
|
@if (effectiveExpanded()) {
|
|
47204
47256
|
<div class="et-query-devtools-json-children">
|
|
47205
|
-
@for (
|
|
47257
|
+
@for (childChunk of childChunks(); track childChunk.start) {
|
|
47258
|
+
<et-query-devtools-json
|
|
47259
|
+
[value]="value()"
|
|
47260
|
+
[chunk]="childChunk"
|
|
47261
|
+
[depth]="depth()"
|
|
47262
|
+
[path]="path()"
|
|
47263
|
+
[search]="search()"
|
|
47264
|
+
[expandedPaths]="expandedPaths()"
|
|
47265
|
+
[collapsedPaths]="collapsedPaths()"
|
|
47266
|
+
[toggleFn]="toggleFn()"
|
|
47267
|
+
/>
|
|
47268
|
+
}
|
|
47269
|
+
@for (entry of visibleEntries(); track entry.k) {
|
|
47206
47270
|
<et-query-devtools-json
|
|
47207
47271
|
[value]="entry.v"
|
|
47208
47272
|
[nodeKey]="entry.k"
|
|
@@ -47238,7 +47302,7 @@ class QueryDevtoolsJsonComponent {
|
|
|
47238
47302
|
</button>
|
|
47239
47303
|
</div>
|
|
47240
47304
|
}
|
|
47241
|
-
`, isInline: true, dependencies: [{ kind: "component", type: QueryDevtoolsJsonComponent, selector: "et-query-devtools-json", inputs: ["value", "nodeKey", "depth", "search", "path", "expandedPaths", "collapsedPaths", "toggleFn"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
47305
|
+
`, isInline: true, dependencies: [{ kind: "component", type: QueryDevtoolsJsonComponent, selector: "et-query-devtools-json", inputs: ["value", "nodeKey", "depth", "search", "chunk", "path", "expandedPaths", "collapsedPaths", "toggleFn"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
47242
47306
|
}
|
|
47243
47307
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: QueryDevtoolsJsonComponent, decorators: [{
|
|
47244
47308
|
type: Component,
|
|
@@ -47255,7 +47319,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
47255
47319
|
type="button"
|
|
47256
47320
|
>
|
|
47257
47321
|
<span class="et-query-devtools-json-caret">{{ effectiveExpanded() ? '▾' : '▸' }}</span>
|
|
47258
|
-
@if (
|
|
47322
|
+
@if (chunk(); as chunk) {
|
|
47323
|
+
<span class="et-query-devtools-json-chunk">{{ chunk.label }}</span>
|
|
47324
|
+
} @else if (nodeKey() !== null) {
|
|
47259
47325
|
<span [class.et-query-devtools-json-hit]="keyHit()" class="et-query-devtools-json-key">{{
|
|
47260
47326
|
nodeKey()
|
|
47261
47327
|
}}</span>
|
|
@@ -47277,7 +47343,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
47277
47343
|
|
|
47278
47344
|
@if (effectiveExpanded()) {
|
|
47279
47345
|
<div class="et-query-devtools-json-children">
|
|
47280
|
-
@for (
|
|
47346
|
+
@for (childChunk of childChunks(); track childChunk.start) {
|
|
47347
|
+
<et-query-devtools-json
|
|
47348
|
+
[value]="value()"
|
|
47349
|
+
[chunk]="childChunk"
|
|
47350
|
+
[depth]="depth()"
|
|
47351
|
+
[path]="path()"
|
|
47352
|
+
[search]="search()"
|
|
47353
|
+
[expandedPaths]="expandedPaths()"
|
|
47354
|
+
[collapsedPaths]="collapsedPaths()"
|
|
47355
|
+
[toggleFn]="toggleFn()"
|
|
47356
|
+
/>
|
|
47357
|
+
}
|
|
47358
|
+
@for (entry of visibleEntries(); track entry.k) {
|
|
47281
47359
|
<et-query-devtools-json
|
|
47282
47360
|
[value]="entry.v"
|
|
47283
47361
|
[nodeKey]="entry.k"
|
|
@@ -47317,7 +47395,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
47317
47395
|
encapsulation: ViewEncapsulation.None,
|
|
47318
47396
|
imports: [QueryDevtoolsJsonComponent],
|
|
47319
47397
|
}]
|
|
47320
|
-
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], nodeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeKey", required: false }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: false }] }], expandedPaths: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedPaths", required: false }] }], collapsedPaths: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedPaths", required: false }] }], toggleFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleFn", required: false }] }] } });
|
|
47398
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], nodeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeKey", required: false }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], search: [{ type: i0.Input, args: [{ isSignal: true, alias: "search", required: false }] }], chunk: [{ type: i0.Input, args: [{ isSignal: true, alias: "chunk", required: false }] }], path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: false }] }], expandedPaths: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedPaths", required: false }] }], collapsedPaths: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedPaths", required: false }] }], toggleFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleFn", required: false }] }] } });
|
|
47399
|
+
// Below the component: an interpolated template literal above an inline template breaks the Angular
|
|
47400
|
+
// language service for the whole template (see `ethlete/no-template-literal-before-inline-template`).
|
|
47401
|
+
const displayOf = (value) => {
|
|
47402
|
+
switch (kindOf(value)) {
|
|
47403
|
+
case 'string':
|
|
47404
|
+
return `"${value}"`;
|
|
47405
|
+
case 'null':
|
|
47406
|
+
return 'null';
|
|
47407
|
+
case 'undefined':
|
|
47408
|
+
return 'undefined';
|
|
47409
|
+
default:
|
|
47410
|
+
return String(value);
|
|
47411
|
+
}
|
|
47412
|
+
};
|
|
47413
|
+
const buildChunks = (entries, window) => {
|
|
47414
|
+
const size = chunkSizeFor(entries.length);
|
|
47415
|
+
const chunks = [];
|
|
47416
|
+
for (let i = 0; i < entries.length; i += size) {
|
|
47417
|
+
const slice = entries.slice(i, i + size);
|
|
47418
|
+
const start = window.offset + i;
|
|
47419
|
+
const end = start + slice.length;
|
|
47420
|
+
const first = window.isArray ? String(start) : (slice[0]?.k ?? '');
|
|
47421
|
+
const last = window.isArray ? String(end - 1) : (slice[slice.length - 1]?.k ?? '');
|
|
47422
|
+
chunks.push({ start, end, label: slice.length === 1 ? first : `${first} … ${last}` });
|
|
47423
|
+
}
|
|
47424
|
+
return chunks;
|
|
47425
|
+
};
|
|
47426
|
+
/** Whether a search term appears anywhere in a subtree - used to keep non-matching slices folded. */
|
|
47427
|
+
const matchesDeep = (value, term) => {
|
|
47428
|
+
if (!value || typeof value !== 'object')
|
|
47429
|
+
return displayOf(value).toLowerCase().includes(term);
|
|
47430
|
+
return entriesOf(value).some(({ k, v }) => k.toLowerCase().includes(term) || matchesDeep(v, term));
|
|
47431
|
+
};
|
|
47321
47432
|
|
|
47322
47433
|
/**
|
|
47323
47434
|
* The global devtools toggle shortcut, rendered for the current platform (`⌘⌥Q` on Apple, where the
|
|
@@ -47371,10 +47482,290 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
|
|
|
47371
47482
|
`, encapsulation: ViewEncapsulation.ShadowDom, styles: [":host{position:fixed;inset-block-end:16px;inset-inline-end:16px;z-index:2147483001}button{--_accent: var(--et-theme-color-primary-solid, #60a5fa);position:relative;display:inline-flex;align-items:center;gap:8px;padding:10px 12px 10px 14px;border:none;border-radius:999px;background:linear-gradient(135deg,#1f1f23,#2a2a30);color:#fafafa;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:600;letter-spacing:.02em;cursor:pointer;box-shadow:0 6px 20px #00000059,inset 0 0 0 1px #ffffff14;transition:transform .15s ease,box-shadow .2s ease}button:before{content:\"\";position:absolute;inset:0;border-radius:inherit;padding:1px;background:linear-gradient(135deg,var(--_accent),transparent 60%);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:.7;transition:opacity .2s ease}button:hover{transform:translateY(-2px);box-shadow:0 10px 28px #0006,0 0 0 4px color-mix(in srgb,var(--_accent) 22%,transparent),inset 0 0 0 1px #ffffff1a}button:hover:before{opacity:1}svg{inline-size:16px;block-size:16px;fill:none;stroke:var(--_accent);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}.label{line-height:1}kbd{padding:3px 6px;border-radius:5px;background:#ffffff12;box-shadow:inset 0 0 0 1px #ffffff1f;color:#d4d4d8;font:inherit;font-size:11px;font-weight:500;line-height:1;white-space:nowrap}\n"] }]
|
|
47372
47483
|
}], propDecorators: { openChange: [{ type: i0.Output, args: ["openChange"] }] } });
|
|
47373
47484
|
|
|
47485
|
+
/** Enough to describe what changed between two responses; past this the list stops being readable. */
|
|
47486
|
+
const MAX_ENTRIES = 200;
|
|
47487
|
+
/**
|
|
47488
|
+
* How deep the walk descends before comparing whole subtrees at once. Guards against a self-referential
|
|
47489
|
+
* response, and keeps a deeply nested change reported as one entry rather than a path per level.
|
|
47490
|
+
*/
|
|
47491
|
+
const MAX_DEPTH = 12;
|
|
47492
|
+
/** Stands in for a path one side does not have, so a stored `undefined` is not mistaken for a missing key. */
|
|
47493
|
+
const ABSENT = /* @__PURE__ */ Symbol('absent');
|
|
47494
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47495
|
+
const valueAt = (record, key) => (key in record ? record[key] : ABSENT);
|
|
47496
|
+
/**
|
|
47497
|
+
* The `id` of every element, when an array holds records keyed by a unique primitive `id` - the shape an
|
|
47498
|
+
* API list almost always has. `null` for anything else, which is then compared by index.
|
|
47499
|
+
*/
|
|
47500
|
+
const recordIds = (items) => {
|
|
47501
|
+
const ids = [];
|
|
47502
|
+
for (const item of items) {
|
|
47503
|
+
if (!isRecord(item))
|
|
47504
|
+
return null;
|
|
47505
|
+
const id = item['id'];
|
|
47506
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
47507
|
+
return null;
|
|
47508
|
+
ids.push(id);
|
|
47509
|
+
}
|
|
47510
|
+
return new Set(ids).size === ids.length ? ids : null;
|
|
47511
|
+
};
|
|
47512
|
+
/**
|
|
47513
|
+
* Whether two values are the same, for the leaves and for the subtrees the walk stopped descending into.
|
|
47514
|
+
* The serialized comparison is key-order sensitive, which only ever over-reports a difference.
|
|
47515
|
+
*/
|
|
47516
|
+
const isSameValue = (before, after) => {
|
|
47517
|
+
if (Object.is(before, after))
|
|
47518
|
+
return true;
|
|
47519
|
+
if (!isRecord(before) && !Array.isArray(before))
|
|
47520
|
+
return false;
|
|
47521
|
+
if (!isRecord(after) && !Array.isArray(after))
|
|
47522
|
+
return false;
|
|
47523
|
+
try {
|
|
47524
|
+
return JSON.stringify(before) === JSON.stringify(after);
|
|
47525
|
+
}
|
|
47526
|
+
catch {
|
|
47527
|
+
return false;
|
|
47528
|
+
}
|
|
47529
|
+
};
|
|
47530
|
+
const push = (acc, entry) => {
|
|
47531
|
+
if (acc.entries.length >= MAX_ENTRIES) {
|
|
47532
|
+
acc.truncated = true;
|
|
47533
|
+
return;
|
|
47534
|
+
}
|
|
47535
|
+
acc.entries.push(entry);
|
|
47536
|
+
};
|
|
47537
|
+
const walk = ({ before, after, path, depth }, acc) => {
|
|
47538
|
+
if (acc.truncated)
|
|
47539
|
+
return;
|
|
47540
|
+
if (before === ABSENT) {
|
|
47541
|
+
push(acc, { path, kind: 'added', before: null, after });
|
|
47542
|
+
return;
|
|
47543
|
+
}
|
|
47544
|
+
if (after === ABSENT) {
|
|
47545
|
+
push(acc, { path, kind: 'removed', before, after: null });
|
|
47546
|
+
return;
|
|
47547
|
+
}
|
|
47548
|
+
if (depth < MAX_DEPTH) {
|
|
47549
|
+
if (isRecord(before) && isRecord(after)) {
|
|
47550
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
47551
|
+
walk({ before: valueAt(before, key), after: valueAt(after, key), path: `${path}.${key}`, depth: depth + 1 }, acc);
|
|
47552
|
+
}
|
|
47553
|
+
return;
|
|
47554
|
+
}
|
|
47555
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
47556
|
+
walkArrays({ before, after, path, depth }, acc);
|
|
47557
|
+
return;
|
|
47558
|
+
}
|
|
47559
|
+
}
|
|
47560
|
+
if (!isSameValue(before, after))
|
|
47561
|
+
push(acc, { path, kind: 'changed', before, after });
|
|
47562
|
+
};
|
|
47563
|
+
const walkArrays = ({ before, after, path, depth }, acc) => {
|
|
47564
|
+
const beforeIds = recordIds(before);
|
|
47565
|
+
const afterIds = recordIds(after);
|
|
47566
|
+
const nested = depth + 1;
|
|
47567
|
+
// Matching records by their `id` keeps a list that gained or lost an item reported as that one item,
|
|
47568
|
+
// instead of every index after it shifting and reading as changed.
|
|
47569
|
+
if (beforeIds && afterIds) {
|
|
47570
|
+
const afterById = new Map(afterIds.map((id, index) => [id, after[index]]));
|
|
47571
|
+
const beforeIdSet = new Set(beforeIds);
|
|
47572
|
+
beforeIds.forEach((id, index) => {
|
|
47573
|
+
const paired = afterById.has(id) ? afterById.get(id) : ABSENT;
|
|
47574
|
+
walk({ before: before[index], after: paired, path: `${path}[id=${id}]`, depth: nested }, acc);
|
|
47575
|
+
});
|
|
47576
|
+
afterIds.forEach((id, index) => {
|
|
47577
|
+
if (beforeIdSet.has(id))
|
|
47578
|
+
return;
|
|
47579
|
+
walk({ before: ABSENT, after: after[index], path: `${path}[id=${id}]`, depth: nested }, acc);
|
|
47580
|
+
});
|
|
47581
|
+
return;
|
|
47582
|
+
}
|
|
47583
|
+
for (let index = 0; index < Math.max(before.length, after.length); index++) {
|
|
47584
|
+
walk({
|
|
47585
|
+
before: index < before.length ? before[index] : ABSENT,
|
|
47586
|
+
after: index < after.length ? after[index] : ABSENT,
|
|
47587
|
+
path: `${path}[${index}]`,
|
|
47588
|
+
depth: nested,
|
|
47589
|
+
}, acc);
|
|
47590
|
+
}
|
|
47591
|
+
};
|
|
47592
|
+
/**
|
|
47593
|
+
* What changed between two responses of the same query, as a flat list of paths - the shape that answers
|
|
47594
|
+
* "the list re-rendered, what changed?" and "did that poll return anything new?".
|
|
47595
|
+
*/
|
|
47596
|
+
const diffQueryDevtoolsResponses = (before, after) => {
|
|
47597
|
+
const acc = { entries: [], truncated: false };
|
|
47598
|
+
walk({ before, after, path: '$', depth: 0 }, acc);
|
|
47599
|
+
return acc;
|
|
47600
|
+
};
|
|
47601
|
+
|
|
47602
|
+
/* eslint-disable @typescript-eslint/naming-convention -- the `__export_*` keys are Insomnia's file format */
|
|
47603
|
+
const WORKSPACE_ID = 'wrk_ethlete_query_devtools';
|
|
47604
|
+
const jsonBody = (body) => {
|
|
47605
|
+
try {
|
|
47606
|
+
return JSON.stringify(body, null, 2);
|
|
47607
|
+
}
|
|
47608
|
+
catch {
|
|
47609
|
+
// A body holding a circular reference or a throwing `toJSON` cannot be exported.
|
|
47610
|
+
return null;
|
|
47611
|
+
}
|
|
47612
|
+
};
|
|
47613
|
+
/**
|
|
47614
|
+
* Insomnia's GraphQL body editor stores `{ query, variables }` as its text, so a GraphQL request is
|
|
47615
|
+
* exported with the document and its variables rather than the serialized POST body.
|
|
47616
|
+
*/
|
|
47617
|
+
const graphqlBody = (gqlQuery, body) => {
|
|
47618
|
+
const variables = body?.variables ?? {};
|
|
47619
|
+
return jsonBody({ query: gqlQuery, variables });
|
|
47620
|
+
};
|
|
47621
|
+
const bodyOf = (request) => {
|
|
47622
|
+
if (request.gqlQuery) {
|
|
47623
|
+
const text = graphqlBody(request.gqlQuery, request.body);
|
|
47624
|
+
return text === null ? {} : { mimeType: 'graphql', text };
|
|
47625
|
+
}
|
|
47626
|
+
if (request.body === null || request.body === undefined)
|
|
47627
|
+
return {};
|
|
47628
|
+
const text = jsonBody(request.body);
|
|
47629
|
+
return text === null ? {} : { mimeType: 'application/json', text };
|
|
47630
|
+
};
|
|
47631
|
+
/**
|
|
47632
|
+
* Angular's `HttpClient` labels an object body `application/json` on its own, so a replay outside the
|
|
47633
|
+
* app needs the header spelled out to reach the same endpoint the same way.
|
|
47634
|
+
*/
|
|
47635
|
+
const headersOf = (input, hasBody) => {
|
|
47636
|
+
const headers = input.headers.filter((header) => header.value !== '');
|
|
47637
|
+
const hasContentType = headers.some((header) => header.name.toLowerCase() === 'content-type');
|
|
47638
|
+
if (!hasBody || hasContentType)
|
|
47639
|
+
return headers;
|
|
47640
|
+
return [...headers, { name: 'Content-Type', value: 'application/json' }];
|
|
47641
|
+
};
|
|
47642
|
+
const AUTH_HEADER = 'Authorization';
|
|
47643
|
+
/**
|
|
47644
|
+
* Insomnia's template tag for reading a value out of another request's response. `when-expired` plus a
|
|
47645
|
+
* max age is what makes the chain maintain itself: once the stored refresh response is older than
|
|
47646
|
+
* `maxAgeSeconds`, sending a request that reads from it re-sends the refresh first.
|
|
47647
|
+
*/
|
|
47648
|
+
const responseTag = (requestId, refresh) => `{% response 'body', '${requestId}', '${refresh.accessTokenPath}', 'when-expired', ${refresh.maxAgeSeconds} %}`;
|
|
47649
|
+
/** Replaces whatever `Authorization` the export resolved with the token the refresh chain yields. */
|
|
47650
|
+
const withChainedAuth = (headers, value) => [
|
|
47651
|
+
{ name: AUTH_HEADER, value },
|
|
47652
|
+
...headers.filter((header) => header.name.toLowerCase() !== AUTH_HEADER.toLowerCase()),
|
|
47653
|
+
];
|
|
47654
|
+
/**
|
|
47655
|
+
* Builds an Insomnia v4 collection from a set of requests, ready to be imported via Insomnia's
|
|
47656
|
+
* `Import > From File / Clipboard`. Requests are filed into a folder per query client, and a request
|
|
47657
|
+
* naming a token refresh gets its bearer token from that refresh's response rather than from a
|
|
47658
|
+
* literal token frozen at export time.
|
|
47659
|
+
*/
|
|
47660
|
+
const buildInsomniaExport = (options) => {
|
|
47661
|
+
const groups = new Map();
|
|
47662
|
+
const resources = [
|
|
47663
|
+
{
|
|
47664
|
+
_id: WORKSPACE_ID,
|
|
47665
|
+
_type: 'workspace',
|
|
47666
|
+
parentId: null,
|
|
47667
|
+
name: options.name,
|
|
47668
|
+
description: 'Exported from the @ethlete/query devtools.',
|
|
47669
|
+
scope: 'collection',
|
|
47670
|
+
created: options.now,
|
|
47671
|
+
modified: options.now,
|
|
47672
|
+
},
|
|
47673
|
+
];
|
|
47674
|
+
const groupIdFor = (name) => {
|
|
47675
|
+
if (!name)
|
|
47676
|
+
return WORKSPACE_ID;
|
|
47677
|
+
const existing = groups.get(name);
|
|
47678
|
+
if (existing)
|
|
47679
|
+
return existing;
|
|
47680
|
+
const id = `fld_${groups.size}`;
|
|
47681
|
+
groups.set(name, id);
|
|
47682
|
+
resources.push({
|
|
47683
|
+
_id: id,
|
|
47684
|
+
_type: 'request_group',
|
|
47685
|
+
parentId: WORKSPACE_ID,
|
|
47686
|
+
name,
|
|
47687
|
+
environment: {},
|
|
47688
|
+
// Insomnia sorts ascending, so the counter keeps folders in the order they were added.
|
|
47689
|
+
metaSortKey: groups.size,
|
|
47690
|
+
created: options.now,
|
|
47691
|
+
modified: options.now,
|
|
47692
|
+
});
|
|
47693
|
+
return id;
|
|
47694
|
+
};
|
|
47695
|
+
const requestResource = (resource) => {
|
|
47696
|
+
resources.push({
|
|
47697
|
+
_type: 'request',
|
|
47698
|
+
description: '',
|
|
47699
|
+
parameters: [],
|
|
47700
|
+
authentication: {},
|
|
47701
|
+
isPrivate: false,
|
|
47702
|
+
settingStoreCookies: true,
|
|
47703
|
+
settingSendCookies: true,
|
|
47704
|
+
settingEncodeUrl: true,
|
|
47705
|
+
settingRebuildPath: true,
|
|
47706
|
+
settingFollowRedirects: 'global',
|
|
47707
|
+
created: options.now,
|
|
47708
|
+
modified: options.now,
|
|
47709
|
+
...resource,
|
|
47710
|
+
});
|
|
47711
|
+
};
|
|
47712
|
+
/** The `Authorization` value a request chained to a given token refresh gets, keyed by refresh id. */
|
|
47713
|
+
const chainedAuth = new Map();
|
|
47714
|
+
// Emitted first so every chained `Authorization` below already has a request id to point at.
|
|
47715
|
+
options.tokenRefreshes?.forEach((refresh, index) => {
|
|
47716
|
+
const id = `req_refresh_${index}`;
|
|
47717
|
+
const body = bodyOf(refresh);
|
|
47718
|
+
chainedAuth.set(refresh.id, `Bearer ${responseTag(id, refresh)}`);
|
|
47719
|
+
requestResource({
|
|
47720
|
+
_id: id,
|
|
47721
|
+
parentId: groupIdFor(refresh.group),
|
|
47722
|
+
name: refresh.name,
|
|
47723
|
+
description: 'Mints the access token every secure request in this collection reads. Insomnia re-sends it on ' +
|
|
47724
|
+
`its own once the stored response is older than ${refresh.maxAgeSeconds}s. The refresh token in ` +
|
|
47725
|
+
'the body is the one the app held at export time - re-export once it is spent.',
|
|
47726
|
+
method: refresh.method,
|
|
47727
|
+
url: refresh.url,
|
|
47728
|
+
body,
|
|
47729
|
+
headers: headersOf(refresh, 'text' in body),
|
|
47730
|
+
// Insomnia sorts ascending and the exported requests start at 0, so the refresh stays on top.
|
|
47731
|
+
metaSortKey: index - (options.tokenRefreshes?.length ?? 0),
|
|
47732
|
+
});
|
|
47733
|
+
});
|
|
47734
|
+
options.requests.forEach((request, index) => {
|
|
47735
|
+
const body = bodyOf(request);
|
|
47736
|
+
const headers = headersOf(request, 'text' in body);
|
|
47737
|
+
const auth = request.secureBy ? chainedAuth.get(request.secureBy) : undefined;
|
|
47738
|
+
requestResource({
|
|
47739
|
+
_id: `req_${index}`,
|
|
47740
|
+
parentId: groupIdFor(request.group),
|
|
47741
|
+
name: request.name,
|
|
47742
|
+
method: request.method,
|
|
47743
|
+
url: request.url,
|
|
47744
|
+
body,
|
|
47745
|
+
headers: auth ? withChainedAuth(headers, auth) : headers,
|
|
47746
|
+
metaSortKey: index,
|
|
47747
|
+
});
|
|
47748
|
+
});
|
|
47749
|
+
return {
|
|
47750
|
+
_type: 'export',
|
|
47751
|
+
__export_format: 4,
|
|
47752
|
+
__export_date: new Date(options.now).toISOString(),
|
|
47753
|
+
__export_source: 'ethlete.query:devtools',
|
|
47754
|
+
resources,
|
|
47755
|
+
};
|
|
47756
|
+
};
|
|
47757
|
+
|
|
47758
|
+
/** How long a copy button stays ticked after a successful write. */
|
|
47759
|
+
const COPIED_RESET_MS = 1200;
|
|
47760
|
+
const noop = () => undefined;
|
|
47374
47761
|
const STORAGE_KEY = 'ethlete:query:devtools:v4';
|
|
47375
47762
|
const MAX_EVENTS = 100;
|
|
47376
47763
|
const DEFAULT_HEIGHT = 360;
|
|
47377
47764
|
const MIN_HEIGHT = 200;
|
|
47765
|
+
/** How many bars the timeline draws. Past this the newest are kept and the rest are counted instead. */
|
|
47766
|
+
const MAX_TIMELINE_ROWS = 200;
|
|
47767
|
+
/** Where the timeline's axis labels sit, as fractions of the window. */
|
|
47768
|
+
const TIMELINE_TICKS = [0, 0.25, 0.5, 0.75, 1];
|
|
47378
47769
|
const readPersistedState = () => {
|
|
47379
47770
|
try {
|
|
47380
47771
|
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
@@ -47408,6 +47799,27 @@ const slimForReport = (value, depth = 0) => {
|
|
|
47408
47799
|
}
|
|
47409
47800
|
return value;
|
|
47410
47801
|
};
|
|
47802
|
+
/** How long an exported Insomnia collection reuses a refresh response whose token lifetime is unknown. */
|
|
47803
|
+
const DEFAULT_TOKEN_MAX_AGE_S = 300;
|
|
47804
|
+
/** Upper bound for the same, so a token that claims a year of life still gets refreshed hourly. */
|
|
47805
|
+
const MAX_TOKEN_MAX_AGE_S = 3600;
|
|
47806
|
+
/**
|
|
47807
|
+
* The JSONPath of a string value inside a response, or `null`. Used to locate the access token in an
|
|
47808
|
+
* auth response whose shape only the provider's `extractTokens` knows.
|
|
47809
|
+
*/
|
|
47810
|
+
const findValuePath = (value, node) => {
|
|
47811
|
+
if (node.value === value)
|
|
47812
|
+
return node.path;
|
|
47813
|
+
if (node.depth > 5 || !node.value || typeof node.value !== 'object')
|
|
47814
|
+
return null;
|
|
47815
|
+
for (const [key, entry] of Object.entries(node.value)) {
|
|
47816
|
+
const path = Array.isArray(node.value) ? `${node.path}[${key}]` : `${node.path}.${key}`;
|
|
47817
|
+
const found = findValuePath(value, { value: entry, path, depth: node.depth + 1 });
|
|
47818
|
+
if (found)
|
|
47819
|
+
return found;
|
|
47820
|
+
}
|
|
47821
|
+
return null;
|
|
47822
|
+
};
|
|
47411
47823
|
/** Best-effort decode of a JWT payload for the auth tab. Returns `null` for anything non-decodable. */
|
|
47412
47824
|
const decodeJwtPayload = (token) => {
|
|
47413
47825
|
if (!token)
|
|
@@ -47439,6 +47851,7 @@ const decodeJwtPayload = (token) => {
|
|
|
47439
47851
|
class QueryDevtoolsComponent {
|
|
47440
47852
|
constructor() {
|
|
47441
47853
|
this.hostEl = inject(ElementRef);
|
|
47854
|
+
this.renderer = injectRenderer();
|
|
47442
47855
|
this.document = inject(DOCUMENT$1);
|
|
47443
47856
|
this.eventIdCounter = 0;
|
|
47444
47857
|
this.lastSelectionKey = '';
|
|
@@ -47447,10 +47860,25 @@ class QueryDevtoolsComponent {
|
|
|
47447
47860
|
{ id: 'queries', label: 'Queries' },
|
|
47448
47861
|
{ id: 'stacks', label: 'Stacks' },
|
|
47449
47862
|
{ id: 'sequences', label: 'Sequences' },
|
|
47863
|
+
{ id: 'forms', label: 'Forms' },
|
|
47450
47864
|
{ id: 'auth', label: 'Auth' },
|
|
47451
47865
|
{ id: 'ws', label: 'Sockets' },
|
|
47452
47866
|
{ id: 'cache', label: 'Cache' },
|
|
47867
|
+
{ id: 'timeline', label: 'Timeline' },
|
|
47453
47868
|
{ id: 'events', label: 'Events' },
|
|
47869
|
+
{ id: 'faults', label: 'Faults' },
|
|
47870
|
+
];
|
|
47871
|
+
this.detailTabs = [
|
|
47872
|
+
{ id: 'overview', label: 'Overview' },
|
|
47873
|
+
{ id: 'history', label: 'History' },
|
|
47874
|
+
{ id: 'data', label: 'Data' },
|
|
47875
|
+
];
|
|
47876
|
+
/** The status chips above the Queries list, in the order a problem is usually looked for. */
|
|
47877
|
+
this.facets = [
|
|
47878
|
+
{ id: 'error', label: 'Failing' },
|
|
47879
|
+
{ id: 'loading', label: 'Loading' },
|
|
47880
|
+
{ id: 'stale', label: 'Stale' },
|
|
47881
|
+
{ id: 'idle', label: 'Idle' },
|
|
47454
47882
|
];
|
|
47455
47883
|
this.shortcut = queryDevtoolsShortcutLabel();
|
|
47456
47884
|
this.open = signal(this.persisted.open ?? false, /* @ts-ignore */
|
|
@@ -47461,15 +47889,32 @@ class QueryDevtoolsComponent {
|
|
|
47461
47889
|
...(ngDevMode ? [{ debugName: "resizing" }] : /* istanbul ignore next */ []));
|
|
47462
47890
|
this.activeTab = signal(this.persisted.activeTab ?? 'queries', /* @ts-ignore */
|
|
47463
47891
|
...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
47892
|
+
/** Which section of the query detail is showing. Shared by the Queries tab and both drawers. */
|
|
47893
|
+
this.detailTab = signal(this.persisted.detailTab ?? 'overview', /* @ts-ignore */
|
|
47894
|
+
...(ngDevMode ? [{ debugName: "detailTab" }] : /* istanbul ignore next */ []));
|
|
47464
47895
|
this.selectedClientName = signal(this.persisted.selectedClientName ?? null, /* @ts-ignore */
|
|
47465
47896
|
...(ngDevMode ? [{ debugName: "selectedClientName" }] : /* istanbul ignore next */ []));
|
|
47466
47897
|
this.selectedQueryId = signal(this.persisted.selectedQueryId ?? null, /* @ts-ignore */
|
|
47467
47898
|
...(ngDevMode ? [{ debugName: "selectedQueryId" }] : /* istanbul ignore next */ []));
|
|
47468
|
-
|
|
47899
|
+
/** The form whose detail the Forms tab has expanded. */
|
|
47900
|
+
this.selectedFormId = signal(this.persisted.selectedFormId ?? null, /* @ts-ignore */
|
|
47901
|
+
...(ngDevMode ? [{ debugName: "selectedFormId" }] : /* istanbul ignore next */ []));
|
|
47902
|
+
// Independent per-drawer selection so no drawer shares the Queries tab's selection - or another
|
|
47903
|
+
// drawer's: opening a query from the Timeline must not also change what the Forms drawer shows.
|
|
47469
47904
|
this.stackSelectedQueryId = signal(null, /* @ts-ignore */
|
|
47470
47905
|
...(ngDevMode ? [{ debugName: "stackSelectedQueryId" }] : /* istanbul ignore next */ []));
|
|
47471
47906
|
this.sequenceSelectedQueryId = signal(null, /* @ts-ignore */
|
|
47472
47907
|
...(ngDevMode ? [{ debugName: "sequenceSelectedQueryId" }] : /* istanbul ignore next */ []));
|
|
47908
|
+
this.formSelectedQueryId = signal(null, /* @ts-ignore */
|
|
47909
|
+
...(ngDevMode ? [{ debugName: "formSelectedQueryId" }] : /* istanbul ignore next */ []));
|
|
47910
|
+
this.timelineSelectedQueryId = signal(null, /* @ts-ignore */
|
|
47911
|
+
...(ngDevMode ? [{ debugName: "timelineSelectedQueryId" }] : /* istanbul ignore next */ []));
|
|
47912
|
+
/** Free-text narrowing of the Queries list. Every whitespace-separated term has to match. */
|
|
47913
|
+
this.queryFilter = signal(this.persisted.queryFilter ?? '', /* @ts-ignore */
|
|
47914
|
+
...(ngDevMode ? [{ debugName: "queryFilter" }] : /* istanbul ignore next */ []));
|
|
47915
|
+
/** The status facets the Queries list is narrowed to. Empty means no status narrowing. */
|
|
47916
|
+
this.queryFacets = signal(new Set(this.persisted.queryFacets ?? []), /* @ts-ignore */
|
|
47917
|
+
...(ngDevMode ? [{ debugName: "queryFacets" }] : /* istanbul ignore next */ []));
|
|
47473
47918
|
this.eventLog = signal([], /* @ts-ignore */
|
|
47474
47919
|
...(ngDevMode ? [{ debugName: "eventLog" }] : /* istanbul ignore next */ []));
|
|
47475
47920
|
/** Keys (`<entryId>:<stepIndex>`) of the sequence steps whose in/out detail is expanded. */
|
|
@@ -47485,6 +47930,9 @@ class QueryDevtoolsComponent {
|
|
|
47485
47930
|
...(ngDevMode ? [{ debugName: "jsonExpandedPaths" }] : /* istanbul ignore next */ []));
|
|
47486
47931
|
this.jsonCollapsedPaths = signal(new Set(this.persisted.jsonCollapsed ?? []), /* @ts-ignore */
|
|
47487
47932
|
...(ngDevMode ? [{ debugName: "jsonCollapsedPaths" }] : /* istanbul ignore next */ []));
|
|
47933
|
+
/** The run whose response the diff is comparing, by run index, or `null` while no diff is open. */
|
|
47934
|
+
this.diffRunIndex = signal(null, /* @ts-ignore */
|
|
47935
|
+
...(ngDevMode ? [{ debugName: "diffRunIndex" }] : /* istanbul ignore next */ []));
|
|
47488
47936
|
/** JIT editor state (response / args editing on the selected query). */
|
|
47489
47937
|
this.editorMode = signal('none', /* @ts-ignore */
|
|
47490
47938
|
...(ngDevMode ? [{ debugName: "editorMode" }] : /* istanbul ignore next */ []));
|
|
@@ -47492,11 +47940,22 @@ class QueryDevtoolsComponent {
|
|
|
47492
47940
|
...(ngDevMode ? [{ debugName: "responseDraft" }] : /* istanbul ignore next */ []));
|
|
47493
47941
|
this.argsDraft = signal('', /* @ts-ignore */
|
|
47494
47942
|
...(ngDevMode ? [{ debugName: "argsDraft" }] : /* istanbul ignore next */ []));
|
|
47943
|
+
/**
|
|
47944
|
+
* The text the currently open editor was seeded with. The textareas bind `value` to this and not to
|
|
47945
|
+
* the live draft: a `value` binding fed by the draft is written back on every keystroke, and writing
|
|
47946
|
+
* a textarea's `value` puts the caret at the end.
|
|
47947
|
+
*/
|
|
47948
|
+
this.editorSeed = '';
|
|
47495
47949
|
this.editError = signal(null, /* @ts-ignore */
|
|
47496
47950
|
...(ngDevMode ? [{ debugName: "editError" }] : /* istanbul ignore next */ []));
|
|
47497
|
-
/** Transient "Copied!" feedback for the copy-report
|
|
47951
|
+
/** Transient "Copied!" feedback for the copy-report, copy-as-Insomnia and copy-document actions. */
|
|
47498
47952
|
this.copiedReport = signal(false, /* @ts-ignore */
|
|
47499
47953
|
...(ngDevMode ? [{ debugName: "copiedReport" }] : /* istanbul ignore next */ []));
|
|
47954
|
+
this.copiedInsomnia = signal(false, /* @ts-ignore */
|
|
47955
|
+
...(ngDevMode ? [{ debugName: "copiedInsomnia" }] : /* istanbul ignore next */ []));
|
|
47956
|
+
this.copiedGql = signal(false, /* @ts-ignore */
|
|
47957
|
+
...(ngDevMode ? [{ debugName: "copiedGql" }] : /* istanbul ignore next */ []));
|
|
47958
|
+
this.copiedReset$ = new Subject();
|
|
47500
47959
|
/** 1-second tick driving the cache freshness countdowns. */
|
|
47501
47960
|
this.clock = toSignal(interval(1000), { initialValue: 0 });
|
|
47502
47961
|
/** "Inspect" mode: hover the live UI to find the query that a component created. */
|
|
@@ -47513,6 +47972,8 @@ class QueryDevtoolsComponent {
|
|
|
47513
47972
|
...(ngDevMode ? [{ debugName: "stackEntries" }] : /* istanbul ignore next */ []));
|
|
47514
47973
|
this.sequenceEntries = computed(() => queryDevtoolsEntries().filter((e) => e.kind === 'query-sequence'), /* @ts-ignore */
|
|
47515
47974
|
...(ngDevMode ? [{ debugName: "sequenceEntries" }] : /* istanbul ignore next */ []));
|
|
47975
|
+
this.formEntries = computed(() => queryDevtoolsEntries().filter((e) => e.kind === 'query-form'), /* @ts-ignore */
|
|
47976
|
+
...(ngDevMode ? [{ debugName: "formEntries" }] : /* istanbul ignore next */ []));
|
|
47516
47977
|
this.authEntries = computed(() => queryDevtoolsEntries().filter((e) => e.kind === 'auth-provider'), /* @ts-ignore */
|
|
47517
47978
|
...(ngDevMode ? [{ debugName: "authEntries" }] : /* istanbul ignore next */ []));
|
|
47518
47979
|
this.wsEntries = computed(() => queryDevtoolsEntries().filter((e) => e.kind === 'ws-client'), /* @ts-ignore */
|
|
@@ -47543,26 +48004,226 @@ class QueryDevtoolsComponent {
|
|
|
47543
48004
|
return Array.from(map, ([repository, info]) => ({ repository, ...info }));
|
|
47544
48005
|
}, /* @ts-ignore */
|
|
47545
48006
|
...(ngDevMode ? [{ debugName: "repositories" }] : /* istanbul ignore next */ []));
|
|
47546
|
-
|
|
48007
|
+
/**
|
|
48008
|
+
* Every client the Faults tab can arm, with the fault it currently carries. Clients with nothing armed
|
|
48009
|
+
* read as {@link EMPTY_QUERY_DEVTOOLS_FAULT} so the inputs always have a value to show.
|
|
48010
|
+
*/
|
|
48011
|
+
this.faultClients = computed(() => {
|
|
48012
|
+
const faults = queryDevtoolsFaults();
|
|
48013
|
+
return this.repositories()
|
|
48014
|
+
.map(({ name, baseUrl }) => {
|
|
48015
|
+
const fault = faults[name] ?? EMPTY_QUERY_DEVTOOLS_FAULT;
|
|
48016
|
+
return { name, baseUrl, fault, armed: isQueryDevtoolsFaultArmed(fault) };
|
|
48017
|
+
})
|
|
48018
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
48019
|
+
}, /* @ts-ignore */
|
|
48020
|
+
...(ngDevMode ? [{ debugName: "faultClients" }] : /* istanbul ignore next */ []));
|
|
48021
|
+
/**
|
|
48022
|
+
* The names of the clients currently carrying a fault, or `null` when none do - so a template can `@if`
|
|
48023
|
+
* on it. Armed faults are the one state where the panel is lying to the app on purpose, and a badge on a
|
|
48024
|
+
* tab that isn't open cannot say that: whichever tab you are reading, a red response has to be
|
|
48025
|
+
* attributable to the injection rather than to the API.
|
|
48026
|
+
*/
|
|
48027
|
+
this.armedFaultClients = computed(() => {
|
|
48028
|
+
const armed = this.faultClients()
|
|
48029
|
+
.filter((client) => client.armed)
|
|
48030
|
+
.map((client) => client.name);
|
|
48031
|
+
return armed.length ? armed : null;
|
|
48032
|
+
}, /* @ts-ignore */
|
|
48033
|
+
...(ngDevMode ? [{ debugName: "armedFaultClients" }] : /* istanbul ignore next */ []));
|
|
48034
|
+
/**
|
|
48035
|
+
* The queries the list is scoped to before the search box and the status chips narrow them further:
|
|
48036
|
+
* either the picked client's, or exactly the inspected element's.
|
|
48037
|
+
*/
|
|
48038
|
+
this.scopedQueries = computed(() => {
|
|
47547
48039
|
const entries = this.queryEntries();
|
|
47548
48040
|
const inspectIds = this.inspectFilterIds();
|
|
47549
|
-
|
|
47550
|
-
|
|
47551
|
-
|
|
47552
|
-
|
|
47553
|
-
|
|
47554
|
-
|
|
47555
|
-
|
|
47556
|
-
}
|
|
47557
|
-
|
|
48041
|
+
if (inspectIds)
|
|
48042
|
+
return entries.filter((e) => inspectIds.includes(e.id));
|
|
48043
|
+
const client = this.selectedClientName();
|
|
48044
|
+
return client ? entries.filter((e) => e.meta.clientName === client) : entries;
|
|
48045
|
+
}, /* @ts-ignore */
|
|
48046
|
+
...(ngDevMode ? [{ debugName: "scopedQueries" }] : /* istanbul ignore next */ []));
|
|
48047
|
+
this.searchedQueries = computed(() => {
|
|
48048
|
+
const items = this.scopedQueries().map((entry) => ({ entry, query: entry.handle }));
|
|
48049
|
+
const terms = this.queryFilter().trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
48050
|
+
if (!terms.length)
|
|
48051
|
+
return items;
|
|
48052
|
+
return items.filter((item) => {
|
|
48053
|
+
const haystack = this.queryHaystack(item.entry, item.query);
|
|
48054
|
+
return terms.every((term) => haystack.includes(term));
|
|
48055
|
+
});
|
|
48056
|
+
}, /* @ts-ignore */
|
|
48057
|
+
...(ngDevMode ? [{ debugName: "searchedQueries" }] : /* istanbul ignore next */ []));
|
|
48058
|
+
/**
|
|
48059
|
+
* How many queries each status chip would leave. Counted before the active chips are applied, so a
|
|
48060
|
+
* chip always states what picking it yields rather than what the current selection happens to show.
|
|
48061
|
+
*/
|
|
48062
|
+
this.facetCounts = computed(() => {
|
|
48063
|
+
// Staleness is a `Date.now()` comparison and deliberately not reactive, so the clock is what makes
|
|
48064
|
+
// the counts age with it - without it a chip would keep the number it happened to be built with.
|
|
48065
|
+
this.clock();
|
|
48066
|
+
const counts = { error: 0, loading: 0, stale: 0, idle: 0 };
|
|
48067
|
+
for (const { query } of this.searchedQueries()) {
|
|
48068
|
+
const status = this.queryStatus(query);
|
|
48069
|
+
if (status === 'error')
|
|
48070
|
+
counts.error++;
|
|
48071
|
+
if (status === 'loading')
|
|
48072
|
+
counts.loading++;
|
|
48073
|
+
if (status === 'idle')
|
|
48074
|
+
counts.idle++;
|
|
48075
|
+
if (this.isStale(query))
|
|
48076
|
+
counts.stale++;
|
|
48077
|
+
}
|
|
48078
|
+
return counts;
|
|
48079
|
+
}, /* @ts-ignore */
|
|
48080
|
+
...(ngDevMode ? [{ debugName: "facetCounts" }] : /* istanbul ignore next */ []));
|
|
48081
|
+
this.filteredQueries = computed(() => {
|
|
48082
|
+
const facets = this.queryFacets();
|
|
48083
|
+
const items = this.searchedQueries();
|
|
48084
|
+
if (!facets.size)
|
|
48085
|
+
return items;
|
|
48086
|
+
// Same reason as in `facetCounts`: a list narrowed to stale queries has to re-evaluate as they age.
|
|
48087
|
+
if (facets.has('stale'))
|
|
48088
|
+
this.clock();
|
|
48089
|
+
return items.filter(({ query }) => this.matchesFacets(query, facets));
|
|
47558
48090
|
}, /* @ts-ignore */
|
|
47559
48091
|
...(ngDevMode ? [{ debugName: "filteredQueries" }] : /* istanbul ignore next */ []));
|
|
48092
|
+
/** How many queries are in scope, which is what the list would show unfiltered. */
|
|
48093
|
+
this.scopedQueryCount = computed(() => this.scopedQueries().length, /* @ts-ignore */
|
|
48094
|
+
...(ngDevMode ? [{ debugName: "scopedQueryCount" }] : /* istanbul ignore next */ []));
|
|
48095
|
+
/** Whether the search box or a status chip is narrowing the list beyond its scope. */
|
|
48096
|
+
this.isQueryListNarrowed = computed(() => !!this.queryFilter().trim() || this.queryFacets().size > 0, /* @ts-ignore */
|
|
48097
|
+
...(ngDevMode ? [{ debugName: "isQueryListNarrowed" }] : /* istanbul ignore next */ []));
|
|
48098
|
+
/**
|
|
48099
|
+
* Every run the scoped queries have recorded, oldest first. The client picker and the inspection
|
|
48100
|
+
* filter narrow the timeline the same way they narrow the Queries list.
|
|
48101
|
+
*/
|
|
48102
|
+
this.scopedRuns = computed(() => {
|
|
48103
|
+
const collected = [];
|
|
48104
|
+
for (const entry of this.scopedQueries()) {
|
|
48105
|
+
for (const run of entry.stats?.runs() ?? [])
|
|
48106
|
+
collected.push({ entry, run });
|
|
48107
|
+
}
|
|
48108
|
+
return collected.sort((a, b) => a.run.startedAt - b.run.startedAt);
|
|
48109
|
+
}, /* @ts-ignore */
|
|
48110
|
+
...(ngDevMode ? [{ debugName: "scopedRuns" }] : /* istanbul ignore next */ []));
|
|
48111
|
+
/**
|
|
48112
|
+
* Every scoped run laid out on one axis, so a stampede reads as overlapping bars and a chain as a
|
|
48113
|
+
* staircase - which is what the Events tab's flat list of wall-clock times cannot show.
|
|
48114
|
+
*/
|
|
48115
|
+
this.timeline = computed(() => {
|
|
48116
|
+
const collected = this.scopedRuns();
|
|
48117
|
+
const hidden = Math.max(0, collected.length - MAX_TIMELINE_ROWS);
|
|
48118
|
+
const shown = hidden ? collected.slice(hidden) : collected;
|
|
48119
|
+
const first = shown[0];
|
|
48120
|
+
if (!first)
|
|
48121
|
+
return { rows: [], startedAt: 0, windowMs: 0, hidden: 0 };
|
|
48122
|
+
// A run in flight has no end yet, so the window has to grow with the clock - otherwise its bar would
|
|
48123
|
+
// freeze at the width it happened to be built with, the same trap `isStale` documents.
|
|
48124
|
+
if (shown.some(({ run }) => run.endedAt === null))
|
|
48125
|
+
this.clock();
|
|
48126
|
+
const now = Date.now();
|
|
48127
|
+
const startedAt = first.run.startedAt;
|
|
48128
|
+
const endedAt = shown.reduce((latest, { run }) => Math.max(latest, run.endedAt ?? now), startedAt);
|
|
48129
|
+
const windowMs = Math.max(1, endedAt - startedAt);
|
|
48130
|
+
const rows = shown.map(({ entry, run }) => {
|
|
48131
|
+
const url = run.url ?? this.requestUrl(entry.handle);
|
|
48132
|
+
return {
|
|
48133
|
+
key: `${entry.id}:${run.index}`,
|
|
48134
|
+
entryId: entry.id,
|
|
48135
|
+
method: entry.meta.method ?? '',
|
|
48136
|
+
path: url ? this.requestPath(url) : (entry.meta.route ?? ''),
|
|
48137
|
+
run,
|
|
48138
|
+
leftPct: ((run.startedAt - startedAt) / windowMs) * 100,
|
|
48139
|
+
// An instant run - a cache entry filled by a poll or by another consumer - still needs a sliver
|
|
48140
|
+
// of width to be visible at all.
|
|
48141
|
+
widthPct: Math.max(0.4, (((run.endedAt ?? now) - run.startedAt) / windowMs) * 100),
|
|
48142
|
+
durationMs: run.endedAt === null ? null : run.endedAt - run.startedAt,
|
|
48143
|
+
};
|
|
48144
|
+
});
|
|
48145
|
+
return { rows, startedAt, windowMs, hidden };
|
|
48146
|
+
}, /* @ts-ignore */
|
|
48147
|
+
...(ngDevMode ? [{ debugName: "timeline" }] : /* istanbul ignore next */ []));
|
|
48148
|
+
/** Axis labels for the timeline, as offsets from the window's start. */
|
|
48149
|
+
this.timelineTicks = computed(() => {
|
|
48150
|
+
const { windowMs, rows } = this.timeline();
|
|
48151
|
+
if (!rows.length)
|
|
48152
|
+
return [];
|
|
48153
|
+
return TIMELINE_TICKS.map((fraction) => ({
|
|
48154
|
+
pct: fraction * 100,
|
|
48155
|
+
label: this.formatDuration(Math.round(windowMs * fraction)),
|
|
48156
|
+
}));
|
|
48157
|
+
}, /* @ts-ignore */
|
|
48158
|
+
...(ngDevMode ? [{ debugName: "timelineTicks" }] : /* istanbul ignore next */ []));
|
|
48159
|
+
/** How many runs every query has recorded, for the Timeline tab's badge. */
|
|
48160
|
+
this.runTotals = computed(() => {
|
|
48161
|
+
let count = 0;
|
|
48162
|
+
let errors = 0;
|
|
48163
|
+
for (const entry of this.queryEntries()) {
|
|
48164
|
+
for (const run of entry.stats?.runs() ?? []) {
|
|
48165
|
+
count++;
|
|
48166
|
+
if (run.status === 'error')
|
|
48167
|
+
errors++;
|
|
48168
|
+
}
|
|
48169
|
+
}
|
|
48170
|
+
return { count, errors };
|
|
48171
|
+
}, /* @ts-ignore */
|
|
48172
|
+
...(ngDevMode ? [{ debugName: "runTotals" }] : /* istanbul ignore next */ []));
|
|
48173
|
+
/** Cache entries across every client, for the Cache tab's badge. */
|
|
48174
|
+
this.cacheEntryCount = computed(() => this.repositories().reduce((total, { repository }) => {
|
|
48175
|
+
// Read the version signal so the badge recounts on every cache mutation.
|
|
48176
|
+
repository.subtle.cacheVersion();
|
|
48177
|
+
return total + repository.subtle.cacheEntries().length;
|
|
48178
|
+
}, 0), /* @ts-ignore */
|
|
48179
|
+
...(ngDevMode ? [{ debugName: "cacheEntryCount" }] : /* istanbul ignore next */ []));
|
|
48180
|
+
/**
|
|
48181
|
+
* What each tab holds, so a failing query in a tab that is not open is still visible. Reading it
|
|
48182
|
+
* subscribes the tab bar to every entry's live state - which is the point of the badges.
|
|
48183
|
+
*/
|
|
48184
|
+
this.tabBadges = computed(() => {
|
|
48185
|
+
const queries = this.queryEntries();
|
|
48186
|
+
const stacks = this.stackEntries();
|
|
48187
|
+
const sequences = this.sequenceEntries();
|
|
48188
|
+
const events = this.eventLog();
|
|
48189
|
+
return {
|
|
48190
|
+
queries: {
|
|
48191
|
+
count: queries.length,
|
|
48192
|
+
errors: queries.filter((entry) => this.queryStatus(entry.handle) === 'error').length,
|
|
48193
|
+
},
|
|
48194
|
+
stacks: {
|
|
48195
|
+
count: stacks.length,
|
|
48196
|
+
errors: stacks.filter((entry) => entry.kind === 'paged-query-stack' ? !!this.asPagedStack(entry).error() : !!this.asStack(entry).anyError()).length,
|
|
48197
|
+
},
|
|
48198
|
+
sequences: {
|
|
48199
|
+
count: sequences.length,
|
|
48200
|
+
errors: sequences.filter((entry) => (this.asSequence(entry)?.failedAt() ?? null) !== null).length,
|
|
48201
|
+
},
|
|
48202
|
+
forms: { count: this.formEntries().length, errors: 0 },
|
|
48203
|
+
auth: { count: this.authEntries().length, errors: 0 },
|
|
48204
|
+
ws: { count: this.wsEntries().length, errors: 0 },
|
|
48205
|
+
cache: { count: this.cacheEntryCount(), errors: 0 },
|
|
48206
|
+
timeline: this.runTotals(),
|
|
48207
|
+
events: {
|
|
48208
|
+
count: events.length,
|
|
48209
|
+
errors: events.filter((event) => event.type === 'request-error').length,
|
|
48210
|
+
},
|
|
48211
|
+
// Armed faults are reported as errors rather than as a plain count: the badge is the one reminder
|
|
48212
|
+
// that the app is misbehaving on purpose, and it has to be impossible to read as "all good".
|
|
48213
|
+
faults: { count: 0, errors: Object.keys(queryDevtoolsFaults()).length, errorNoun: 'armed' },
|
|
48214
|
+
};
|
|
48215
|
+
}, /* @ts-ignore */
|
|
48216
|
+
...(ngDevMode ? [{ debugName: "tabBadges" }] : /* istanbul ignore next */ []));
|
|
47560
48217
|
this.selectedQuery = computed(() => this.findQuery(this.selectedQueryId()), /* @ts-ignore */
|
|
47561
48218
|
...(ngDevMode ? [{ debugName: "selectedQuery" }] : /* istanbul ignore next */ []));
|
|
47562
48219
|
this.stackSelectedQuery = computed(() => this.findQuery(this.stackSelectedQueryId()), /* @ts-ignore */
|
|
47563
48220
|
...(ngDevMode ? [{ debugName: "stackSelectedQuery" }] : /* istanbul ignore next */ []));
|
|
47564
48221
|
this.sequenceSelectedQuery = computed(() => this.findQuery(this.sequenceSelectedQueryId()), /* @ts-ignore */
|
|
47565
48222
|
...(ngDevMode ? [{ debugName: "sequenceSelectedQuery" }] : /* istanbul ignore next */ []));
|
|
48223
|
+
this.formSelectedQuery = computed(() => this.findQuery(this.formSelectedQueryId()), /* @ts-ignore */
|
|
48224
|
+
...(ngDevMode ? [{ debugName: "formSelectedQuery" }] : /* istanbul ignore next */ []));
|
|
48225
|
+
this.timelineSelectedQuery = computed(() => this.findQuery(this.timelineSelectedQueryId()), /* @ts-ignore */
|
|
48226
|
+
...(ngDevMode ? [{ debugName: "timelineSelectedQuery" }] : /* istanbul ignore next */ []));
|
|
47566
48227
|
this.cacheView = computed(() => this.repositories().map(({ repository, name, baseUrl, client }) => {
|
|
47567
48228
|
// Read the version signal so this recomputes on every cache mutation.
|
|
47568
48229
|
repository.subtle.cacheVersion();
|
|
@@ -47592,6 +48253,15 @@ class QueryDevtoolsComponent {
|
|
|
47592
48253
|
return map;
|
|
47593
48254
|
}, /* @ts-ignore */
|
|
47594
48255
|
...(ngDevMode ? [{ debugName: "elementQueryMap" }] : /* istanbul ignore next */ []));
|
|
48256
|
+
// --- Fault injection ---
|
|
48257
|
+
this.FAULT_STATUSES = QUERY_DEVTOOLS_FAULT_STATUSES;
|
|
48258
|
+
this.CLEAR_FAULTS = clearQueryDevtoolsFaults;
|
|
48259
|
+
/** The ceiling each numeric fault field is clamped to. */
|
|
48260
|
+
this.FAULT_LIMITS = {
|
|
48261
|
+
latencyMs: 60_000,
|
|
48262
|
+
failNext: 99,
|
|
48263
|
+
failRate: 100,
|
|
48264
|
+
};
|
|
47595
48265
|
// Assigned here (not as an arrow property) so `this` is bound for the value-explorer callback.
|
|
47596
48266
|
this.toggleJsonPath = (path, expand) => {
|
|
47597
48267
|
const expanded = new Set(this.jsonExpandedPaths());
|
|
@@ -47607,6 +48277,14 @@ class QueryDevtoolsComponent {
|
|
|
47607
48277
|
this.jsonExpandedPaths.set(expanded);
|
|
47608
48278
|
this.jsonCollapsedPaths.set(collapsed);
|
|
47609
48279
|
};
|
|
48280
|
+
// Each copy restarts the countdown; switchMap drops the pending reset of the previous one.
|
|
48281
|
+
this.copiedReset$
|
|
48282
|
+
.pipe(switchMap(() => timer(COPIED_RESET_MS)), tap(() => {
|
|
48283
|
+
this.copiedReport.set(false);
|
|
48284
|
+
this.copiedInsomnia.set(false);
|
|
48285
|
+
this.copiedGql.set(false);
|
|
48286
|
+
}), takeUntilDestroyed())
|
|
48287
|
+
.subscribe();
|
|
47610
48288
|
// Merge every live repository's event stream into the rolling log, re-subscribing as the set of
|
|
47611
48289
|
// repositories changes. Composed with RxJS (not a subscribe-in-effect) per the styleguide.
|
|
47612
48290
|
toObservable(this.repositories)
|
|
@@ -47617,9 +48295,13 @@ class QueryDevtoolsComponent {
|
|
|
47617
48295
|
open: this.open(),
|
|
47618
48296
|
height: this.panelHeight(),
|
|
47619
48297
|
activeTab: this.activeTab(),
|
|
48298
|
+
detailTab: this.detailTab(),
|
|
47620
48299
|
selectedClientName: this.selectedClientName(),
|
|
47621
48300
|
selectedQueryId: this.selectedQueryId(),
|
|
48301
|
+
selectedFormId: this.selectedFormId(),
|
|
47622
48302
|
inspectFilterIds: this.inspectFilterIds(),
|
|
48303
|
+
queryFilter: this.queryFilter(),
|
|
48304
|
+
queryFacets: [...this.queryFacets()],
|
|
47623
48305
|
jsonSearch: this.jsonSearch(),
|
|
47624
48306
|
expandedSteps: [...this.expandedSteps()],
|
|
47625
48307
|
jsonExpanded: [...this.jsonExpandedPaths()],
|
|
@@ -47642,6 +48324,8 @@ class QueryDevtoolsComponent {
|
|
|
47642
48324
|
this.editorMode.set('none');
|
|
47643
48325
|
this.editError.set(null);
|
|
47644
48326
|
this.copiedReport.set(false);
|
|
48327
|
+
this.copiedInsomnia.set(false);
|
|
48328
|
+
this.diffRunIndex.set(null);
|
|
47645
48329
|
if (key !== this.lastSelectionKey) {
|
|
47646
48330
|
this.lastSelectionKey = key;
|
|
47647
48331
|
this.jsonSearch.set('');
|
|
@@ -47690,6 +48374,17 @@ class QueryDevtoolsComponent {
|
|
|
47690
48374
|
clearInspectFilter() {
|
|
47691
48375
|
this.inspectFilterIds.set(null);
|
|
47692
48376
|
}
|
|
48377
|
+
toggleFacet(facet) {
|
|
48378
|
+
const next = new Set(this.queryFacets());
|
|
48379
|
+
if (!next.delete(facet))
|
|
48380
|
+
next.add(facet);
|
|
48381
|
+
this.queryFacets.set(next);
|
|
48382
|
+
}
|
|
48383
|
+
/** Drops the search term and the status chips, keeping the client / inspection scope. */
|
|
48384
|
+
clearQueryFilters() {
|
|
48385
|
+
this.queryFilter.set('');
|
|
48386
|
+
this.queryFacets.set(new Set());
|
|
48387
|
+
}
|
|
47693
48388
|
toggleInspect() {
|
|
47694
48389
|
this.inspectActive.update((v) => !v);
|
|
47695
48390
|
}
|
|
@@ -47699,10 +48394,44 @@ class QueryDevtoolsComponent {
|
|
|
47699
48394
|
}
|
|
47700
48395
|
inspectLabel(entries) {
|
|
47701
48396
|
const first = entries[0];
|
|
47702
|
-
if (entries.length === 1 && first)
|
|
47703
|
-
return `${first.meta.method ?? ''} ${first.
|
|
48397
|
+
if (entries.length === 1 && first) {
|
|
48398
|
+
return `${first.meta.method ?? ''} ${this.queryRoute(first, first.handle)}`.trim();
|
|
48399
|
+
}
|
|
47704
48400
|
return `${entries.length} queries`;
|
|
47705
48401
|
}
|
|
48402
|
+
/**
|
|
48403
|
+
* A query's route, split so the template can tell its static path from its path params (each carrying
|
|
48404
|
+
* the value the query used, or `:<name>` while it has none yet) and from the query string of the
|
|
48405
|
+
* request that ran - which is what tells two requests to the same endpoint apart.
|
|
48406
|
+
*/
|
|
48407
|
+
routeSegments(entry, query) {
|
|
48408
|
+
const parts = entry?.meta.routeParts;
|
|
48409
|
+
const search = query.subtle.request()?.url.split('?')[1];
|
|
48410
|
+
const querySegment = search ? [{ text: `?${search}`, kind: 'query' }] : [];
|
|
48411
|
+
if (!parts?.length) {
|
|
48412
|
+
return entry?.meta.route ? [{ text: entry.meta.route, kind: 'static' }, ...querySegment] : [];
|
|
48413
|
+
}
|
|
48414
|
+
const pathParams = this.queryArgs(query)?.['pathParams'];
|
|
48415
|
+
const pathSegments = parts.map(({ text, param }) => {
|
|
48416
|
+
if (!param)
|
|
48417
|
+
return { text, kind: 'static' };
|
|
48418
|
+
const value = pathParams?.[param];
|
|
48419
|
+
return { text: value === undefined || value === null ? `:${param}` : String(value), kind: 'param', name: param };
|
|
48420
|
+
});
|
|
48421
|
+
return [...pathSegments, ...querySegment];
|
|
48422
|
+
}
|
|
48423
|
+
/** The full URL of the request a query last made, or `null` while it has not executed. */
|
|
48424
|
+
requestUrl(query) {
|
|
48425
|
+
return query.subtle.request()?.url ?? null;
|
|
48426
|
+
}
|
|
48427
|
+
/**
|
|
48428
|
+
* The args of a query. A query executed imperatively (`execute({ args })`, a sequence step, an auth
|
|
48429
|
+
* query) never writes them to its own `args` signal - only the `withArgs` feature does - so the args
|
|
48430
|
+
* its current request was built from stand in.
|
|
48431
|
+
*/
|
|
48432
|
+
queryArgs(query) {
|
|
48433
|
+
return query.args() ?? query.subtle.request()?.args ?? null;
|
|
48434
|
+
}
|
|
47706
48435
|
queryStatus(query) {
|
|
47707
48436
|
const state = query.executionState();
|
|
47708
48437
|
if (!state)
|
|
@@ -47713,14 +48442,164 @@ class QueryDevtoolsComponent {
|
|
|
47713
48442
|
return 'error';
|
|
47714
48443
|
return 'success';
|
|
47715
48444
|
}
|
|
48445
|
+
/**
|
|
48446
|
+
* A request in flight is already refreshing, so reporting it as stale on top of `loading` is noise -
|
|
48447
|
+
* the same precedence the cache tab's freshness column applies.
|
|
48448
|
+
*/
|
|
47716
48449
|
isStale(query) {
|
|
47717
48450
|
try {
|
|
47718
|
-
|
|
48451
|
+
const request = query.subtle.request();
|
|
48452
|
+
if (!request || request.loading())
|
|
48453
|
+
return false;
|
|
48454
|
+
return request.isStale();
|
|
47719
48455
|
}
|
|
47720
48456
|
catch {
|
|
47721
48457
|
return false;
|
|
47722
48458
|
}
|
|
47723
48459
|
}
|
|
48460
|
+
/**
|
|
48461
|
+
* What a query's current request is doing beyond being loading, or `null` when there is nothing beyond
|
|
48462
|
+
* the status dot to say - so the readout only takes up room while it carries something.
|
|
48463
|
+
*/
|
|
48464
|
+
requestProgress(query) {
|
|
48465
|
+
const request = query.subtle.request();
|
|
48466
|
+
if (!request)
|
|
48467
|
+
return null;
|
|
48468
|
+
const retry = request.subtle.retryState();
|
|
48469
|
+
const attempts = request.subtle.attempts();
|
|
48470
|
+
// The request's own loading state, not the query's: a forced loading state carries no progress, and
|
|
48471
|
+
// reading the query's would report the forced one as a transfer that never started.
|
|
48472
|
+
const progress = request.loading()?.progress ?? null;
|
|
48473
|
+
if (!retry && !progress && attempts < 2)
|
|
48474
|
+
return null;
|
|
48475
|
+
// The countdown is a `Date.now()` comparison, so the clock is what makes it tick down - the same trap
|
|
48476
|
+
// `isStale` documents.
|
|
48477
|
+
this.clock();
|
|
48478
|
+
return { attempts, retry, retryInMs: retry ? Math.max(0, retry.startsAt - Date.now()) : null, progress };
|
|
48479
|
+
}
|
|
48480
|
+
/** Why a retry was scheduled, as the panel spells it out. A status of 0 never reached the server. */
|
|
48481
|
+
retryCause(status) {
|
|
48482
|
+
return status ? `after ${status}` : 'after a connection failure';
|
|
48483
|
+
}
|
|
48484
|
+
formatPercent(percentage) {
|
|
48485
|
+
return `${Math.round(percentage)}%`;
|
|
48486
|
+
}
|
|
48487
|
+
/** A countdown in whole seconds, spelled out the way the cache freshness column does. */
|
|
48488
|
+
formatCountdown(ms) {
|
|
48489
|
+
return ms === null ? '—' : `${Math.ceil(ms / 1000)}s`;
|
|
48490
|
+
}
|
|
48491
|
+
queryActivity(entry) {
|
|
48492
|
+
return this.activityOf([entry.stats]);
|
|
48493
|
+
}
|
|
48494
|
+
linkActivity(link) {
|
|
48495
|
+
return this.activityOf([link.stats]);
|
|
48496
|
+
}
|
|
48497
|
+
stackActivity(stack) {
|
|
48498
|
+
return this.activityOf(this.queriesForStack(stack).map((link) => link.stats));
|
|
48499
|
+
}
|
|
48500
|
+
sequenceActivity(sequence) {
|
|
48501
|
+
return this.activityOf(this.queriesForSequence(sequence).map((link) => link.stats));
|
|
48502
|
+
}
|
|
48503
|
+
/** Clears an entry's counters and run history, so the next interaction can be measured on its own. */
|
|
48504
|
+
resetStats(entry) {
|
|
48505
|
+
entry.stats?.reset();
|
|
48506
|
+
this.diffRunIndex.set(null);
|
|
48507
|
+
}
|
|
48508
|
+
/** The same, for every query the timeline covers. */
|
|
48509
|
+
resetTimeline() {
|
|
48510
|
+
for (const entry of this.scopedQueries())
|
|
48511
|
+
entry.stats?.reset();
|
|
48512
|
+
this.diffRunIndex.set(null);
|
|
48513
|
+
}
|
|
48514
|
+
/** A query's runs, newest first - the order a history is read in. */
|
|
48515
|
+
queryRuns(entry) {
|
|
48516
|
+
return [...(entry.stats?.runs() ?? [])].reverse();
|
|
48517
|
+
}
|
|
48518
|
+
/**
|
|
48519
|
+
* What a run's status dot and timeline bar colour by. `pending` reuses the panel's loading colour;
|
|
48520
|
+
* `aborted` matches no rule and so falls back to the neutral one.
|
|
48521
|
+
*/
|
|
48522
|
+
runStatus(run) {
|
|
48523
|
+
return run.status === 'pending' ? 'loading' : run.status;
|
|
48524
|
+
}
|
|
48525
|
+
/** Whether a run can be diffed: it still holds its body, and so does an older run of the same query. */
|
|
48526
|
+
canDiffRun(entry, run) {
|
|
48527
|
+
if (!run.hasResponse)
|
|
48528
|
+
return false;
|
|
48529
|
+
return (entry.stats?.runs() ?? []).some((other) => other.hasResponse && other.index < run.index);
|
|
48530
|
+
}
|
|
48531
|
+
toggleRunDiff(run) {
|
|
48532
|
+
this.diffRunIndex.update((current) => (current === run.index ? null : run.index));
|
|
48533
|
+
}
|
|
48534
|
+
/**
|
|
48535
|
+
* The response diff of the picked run against the newest older run that still holds a body - which is
|
|
48536
|
+
* not necessarily the run right before it, since a failed run has none to compare.
|
|
48537
|
+
*
|
|
48538
|
+
* `null` unless a run is picked, so a closed diff costs nothing to walk.
|
|
48539
|
+
*/
|
|
48540
|
+
responseDiff(entry) {
|
|
48541
|
+
const picked = this.diffRunIndex();
|
|
48542
|
+
if (picked === null)
|
|
48543
|
+
return null;
|
|
48544
|
+
const runs = entry.stats?.runs() ?? [];
|
|
48545
|
+
const at = runs.findIndex((run) => run.index === picked);
|
|
48546
|
+
const after = at === -1 ? null : runs[at];
|
|
48547
|
+
if (!after?.hasResponse)
|
|
48548
|
+
return null;
|
|
48549
|
+
for (let index = at - 1; index >= 0; index--) {
|
|
48550
|
+
const before = runs[index];
|
|
48551
|
+
if (before?.hasResponse) {
|
|
48552
|
+
return { before, after, diff: diffQueryDevtoolsResponses(before.response, after.response) };
|
|
48553
|
+
}
|
|
48554
|
+
}
|
|
48555
|
+
return null;
|
|
48556
|
+
}
|
|
48557
|
+
/** Opens a query in the Queries tab - the Events tab is a way in, not a dead end. */
|
|
48558
|
+
selectQuery(id) {
|
|
48559
|
+
this.activeTab.set('queries');
|
|
48560
|
+
this.selectedQueryId.set(id);
|
|
48561
|
+
}
|
|
48562
|
+
selectTimelineRow(row) {
|
|
48563
|
+
this.timelineSelectedQueryId.set(row.entryId);
|
|
48564
|
+
}
|
|
48565
|
+
selectEventRow(item) {
|
|
48566
|
+
if (item.queryId)
|
|
48567
|
+
this.selectQuery(item.queryId);
|
|
48568
|
+
}
|
|
48569
|
+
/** A value on one line, for a diff row or a form field. The full tree would bury the row it sits in. */
|
|
48570
|
+
inlineValue(value) {
|
|
48571
|
+
if (typeof value === 'string')
|
|
48572
|
+
return value.length > 80 ? `"${value.slice(0, 80)}…"` : `"${value}"`;
|
|
48573
|
+
if (value === undefined)
|
|
48574
|
+
return 'undefined';
|
|
48575
|
+
try {
|
|
48576
|
+
const json = JSON.stringify(value) ?? 'undefined';
|
|
48577
|
+
return json.length > 80 ? `${json.slice(0, 80)}…` : json;
|
|
48578
|
+
}
|
|
48579
|
+
catch {
|
|
48580
|
+
return '[unserializable]';
|
|
48581
|
+
}
|
|
48582
|
+
}
|
|
48583
|
+
/** A byte count the way a network panel spells one out. */
|
|
48584
|
+
formatBytes(bytes) {
|
|
48585
|
+
if (bytes < 1000)
|
|
48586
|
+
return `${bytes} B`;
|
|
48587
|
+
if (bytes < 1_000_000)
|
|
48588
|
+
return `${(bytes / 1000).toFixed(1)} kB`;
|
|
48589
|
+
return `${(bytes / 1_000_000).toFixed(2)} MB`;
|
|
48590
|
+
}
|
|
48591
|
+
/**
|
|
48592
|
+
* A transferred size, marked `≈` when any part of it was measured from a decoded body instead of read
|
|
48593
|
+
* from a `content-length` header - such a size ignores transport compression.
|
|
48594
|
+
*/
|
|
48595
|
+
formatTransferred(bytes, isEstimated) {
|
|
48596
|
+
return `${isEstimated ? '≈' : ''}${this.formatBytes(bytes)}`;
|
|
48597
|
+
}
|
|
48598
|
+
formatDuration(ms) {
|
|
48599
|
+
if (ms === null)
|
|
48600
|
+
return '—';
|
|
48601
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
|
|
48602
|
+
}
|
|
47724
48603
|
executeQuery(query, allowCache) {
|
|
47725
48604
|
query.execute(allowCache ? { options: { allowCache: true } } : undefined);
|
|
47726
48605
|
}
|
|
@@ -47736,11 +48615,18 @@ class QueryDevtoolsComponent {
|
|
|
47736
48615
|
const error = query.error();
|
|
47737
48616
|
const httpStatus = error ? error.raw.status : this.responseStatus(query);
|
|
47738
48617
|
const method = entry.meta.method ?? '';
|
|
47739
|
-
|
|
47740
|
-
|
|
48618
|
+
// The URL actually requested already contains the base URL and the resolved params, so the client
|
|
48619
|
+
// is only spelled out separately when the query has not run and only its `:param` template is known.
|
|
48620
|
+
const requestUrl = this.requestUrl(query);
|
|
48621
|
+
const route = requestUrl ?? entry.meta.route ?? '-';
|
|
48622
|
+
const client = requestUrl ? '' : (entry.meta.clientBaseUrl ?? entry.meta.clientName ?? '');
|
|
47741
48623
|
const statusLine = `status: ${this.queryStatus(query)}${httpStatus !== null ? ` (${httpStatus})` : ''} · ${this.formatTime(query.lastTimeExecutedAt())}`;
|
|
48624
|
+
const features = entry.meta.features?.length
|
|
48625
|
+
? `features: ${entry.meta.features.map((feature) => this.featureSummary(feature)).join(' | ')}`
|
|
48626
|
+
: null;
|
|
48627
|
+
const activity = this.activitySummary(this.queryActivity(entry));
|
|
47742
48628
|
const gqlDoc = entry.meta.gqlQuery ? this.gqlDocument(entry.meta.gqlQuery) : null;
|
|
47743
|
-
const args =
|
|
48629
|
+
const args = this.queryArgs(query);
|
|
47744
48630
|
const argsLabel = gqlDoc ? 'Variables' : 'Args';
|
|
47745
48631
|
const argsJson = args !== null && args !== undefined ? JSON.stringify(args, null, 2) : null;
|
|
47746
48632
|
const bodyLabel = error ? `Error (${error.raw.status})` : 'Response';
|
|
@@ -47750,6 +48636,10 @@ class QueryDevtoolsComponent {
|
|
|
47750
48636
|
: error.error.message
|
|
47751
48637
|
: JSON.stringify(slimForReport(query.response()), null, 2);
|
|
47752
48638
|
const textParts = [`${method} ${route}${client ? ` - ${client}` : ''}`, statusLine];
|
|
48639
|
+
if (activity)
|
|
48640
|
+
textParts.push(activity);
|
|
48641
|
+
if (features)
|
|
48642
|
+
textParts.push(features);
|
|
47753
48643
|
if (gqlDoc)
|
|
47754
48644
|
textParts.push('', 'GraphQL document', gqlDoc);
|
|
47755
48645
|
if (argsJson)
|
|
@@ -47761,22 +48651,64 @@ class QueryDevtoolsComponent {
|
|
|
47761
48651
|
`<b>${esc(method)}</b> <code>${esc(route)}</code>${client ? ` - <code>${esc(client)}</code>` : ''}`,
|
|
47762
48652
|
esc(statusLine),
|
|
47763
48653
|
];
|
|
48654
|
+
if (activity)
|
|
48655
|
+
htmlParts.push(esc(activity));
|
|
48656
|
+
if (features)
|
|
48657
|
+
htmlParts.push(esc(features));
|
|
47764
48658
|
if (gqlDoc)
|
|
47765
48659
|
htmlParts.push('<b>GraphQL document</b>', `<pre><code>${esc(gqlDoc)}</code></pre>`);
|
|
47766
48660
|
if (argsJson)
|
|
47767
48661
|
htmlParts.push(`<b>${argsLabel}</b>`, `<pre><code>${esc(argsJson)}</code></pre>`);
|
|
47768
48662
|
htmlParts.push(`<b>${esc(bodyLabel)}</b>`, `<pre><code>${esc(bodyContent)}</code></pre>`);
|
|
47769
48663
|
const html = htmlParts.join('<br>');
|
|
47770
|
-
this.writeToClipboard(html, text);
|
|
48664
|
+
this.writeToClipboard({ html, text }, this.copiedReport);
|
|
48665
|
+
}
|
|
48666
|
+
// --- Insomnia export ---
|
|
48667
|
+
/** Copies one query as an Insomnia collection, for `Import > From Clipboard`. */
|
|
48668
|
+
copyInsomniaRequest(entry, query) {
|
|
48669
|
+
const requests = [this.insomniaRequest(entry, query)];
|
|
48670
|
+
const json = JSON.stringify(buildInsomniaExport({
|
|
48671
|
+
name: `${entry.meta.clientName ?? 'query'} · ${this.queryRoute(entry, query) || 'request'}`,
|
|
48672
|
+
requests,
|
|
48673
|
+
tokenRefreshes: this.insomniaTokenRefreshes(requests),
|
|
48674
|
+
now: Date.now(),
|
|
48675
|
+
}), null, 2);
|
|
48676
|
+
this.writeToClipboard({ text: json }, this.copiedInsomnia);
|
|
48677
|
+
}
|
|
48678
|
+
/** Copies the GraphQL document as displayed — dedented, so it pastes straight into a playground. */
|
|
48679
|
+
copyGqlDocument(doc) {
|
|
48680
|
+
this.writeToClipboard({ text: this.gqlDocument(doc) }, this.copiedGql);
|
|
48681
|
+
}
|
|
48682
|
+
/**
|
|
48683
|
+
* Downloads every query currently listed (so the client filter and the inspection filter both apply)
|
|
48684
|
+
* as one Insomnia collection, filed into a folder per query client.
|
|
48685
|
+
*/
|
|
48686
|
+
downloadInsomniaCollection() {
|
|
48687
|
+
const items = this.filteredQueries();
|
|
48688
|
+
if (!items.length)
|
|
48689
|
+
return;
|
|
48690
|
+
const client = this.selectedClientName();
|
|
48691
|
+
const requests = items.map(({ entry, query }) => this.insomniaRequest(entry, query));
|
|
48692
|
+
const json = JSON.stringify(buildInsomniaExport({
|
|
48693
|
+
name: `${client ?? 'ethlete'} queries`,
|
|
48694
|
+
requests,
|
|
48695
|
+
tokenRefreshes: this.insomniaTokenRefreshes(requests),
|
|
48696
|
+
now: Date.now(),
|
|
48697
|
+
}), null, 2);
|
|
48698
|
+
this.downloadFile(`insomnia-${client ?? 'ethlete'}-queries.json`, json);
|
|
47771
48699
|
}
|
|
47772
48700
|
// --- JIT editing ---
|
|
47773
48701
|
openResponseEditor(query) {
|
|
47774
|
-
|
|
48702
|
+
const draft = JSON.stringify(query.response() ?? null, null, 2);
|
|
48703
|
+
this.editorSeed = draft;
|
|
48704
|
+
this.responseDraft.set(draft);
|
|
47775
48705
|
this.editError.set(null);
|
|
47776
48706
|
this.editorMode.set('response');
|
|
47777
48707
|
}
|
|
47778
48708
|
openArgsEditor(query) {
|
|
47779
|
-
|
|
48709
|
+
const draft = JSON.stringify(this.queryArgs(query) ?? {}, null, 2);
|
|
48710
|
+
this.editorSeed = draft;
|
|
48711
|
+
this.argsDraft.set(draft);
|
|
47780
48712
|
this.editError.set(null);
|
|
47781
48713
|
this.editorMode.set('args');
|
|
47782
48714
|
}
|
|
@@ -47829,6 +48761,22 @@ class QueryDevtoolsComponent {
|
|
|
47829
48761
|
query.subtle.setLoading(null);
|
|
47830
48762
|
query.subtle.setError(null);
|
|
47831
48763
|
}
|
|
48764
|
+
/**
|
|
48765
|
+
* Arms one numeric field of a client's fault from an input's raw value. Clamped here rather than left
|
|
48766
|
+
* to the input's `min`/`max`, which a typed-in (or pasted) value ignores.
|
|
48767
|
+
*/
|
|
48768
|
+
armFaultValue(options) {
|
|
48769
|
+
const { clientName, field, value } = options;
|
|
48770
|
+
const parsed = clamp$1(Math.trunc(Number(value) || 0), 0, this.FAULT_LIMITS[field]);
|
|
48771
|
+
setQueryDevtoolsFault({ clientName, patch: { [field]: parsed } });
|
|
48772
|
+
}
|
|
48773
|
+
armFaultStatus(options) {
|
|
48774
|
+
setQueryDevtoolsFault({ clientName: options.clientName, patch: { status: Number(options.value) } });
|
|
48775
|
+
}
|
|
48776
|
+
/** Whether the default retry policy retries the status a client is armed to fail with. */
|
|
48777
|
+
isFaultStatusRetryable(status) {
|
|
48778
|
+
return QUERY_DEVTOOLS_FAULT_STATUSES.find((entry) => entry.status === status)?.retryable ?? false;
|
|
48779
|
+
}
|
|
47832
48780
|
// --- Cache actions ---
|
|
47833
48781
|
refetchCacheEntry(entry) {
|
|
47834
48782
|
entry.request.execute();
|
|
@@ -47906,6 +48854,50 @@ class QueryDevtoolsComponent {
|
|
|
47906
48854
|
asWs(entry) {
|
|
47907
48855
|
return entry.handle;
|
|
47908
48856
|
}
|
|
48857
|
+
asForm(entry) {
|
|
48858
|
+
return entry.handle;
|
|
48859
|
+
}
|
|
48860
|
+
/**
|
|
48861
|
+
* The queries a form feeds, discovered from the reads its `value()` recorded while their args were
|
|
48862
|
+
* built - so a form that nothing consumes yet reads as exactly that.
|
|
48863
|
+
*/
|
|
48864
|
+
queriesDrivenByForm(entry) {
|
|
48865
|
+
return this.queryEntries()
|
|
48866
|
+
.filter((candidate) => candidate.formLinks?.ids().includes(entry.id))
|
|
48867
|
+
.map((candidate) => this.queryLinkFor(candidate));
|
|
48868
|
+
}
|
|
48869
|
+
/** The reverse: the forms whose value a query's args read. */
|
|
48870
|
+
formsDrivingQuery(entry) {
|
|
48871
|
+
const ids = entry.formLinks?.ids() ?? [];
|
|
48872
|
+
return this.formEntries().filter((form) => ids.includes(form.id));
|
|
48873
|
+
}
|
|
48874
|
+
selectForm(id) {
|
|
48875
|
+
this.activeTab.set('forms');
|
|
48876
|
+
this.selectedFormId.set(id);
|
|
48877
|
+
}
|
|
48878
|
+
/**
|
|
48879
|
+
* The refreshes that re-executed a query, newest first - the answer to "why did this refetch?". Read
|
|
48880
|
+
* off the event log, so it goes back exactly as far as the log does.
|
|
48881
|
+
*/
|
|
48882
|
+
refreshesFor(entryId) {
|
|
48883
|
+
return this.eventLog()
|
|
48884
|
+
.filter((item) => item.cause && item.refreshed?.some((refreshed) => refreshed.queryIds.includes(entryId)))
|
|
48885
|
+
.map((item) => ({
|
|
48886
|
+
id: item.id,
|
|
48887
|
+
timestamp: item.timestamp,
|
|
48888
|
+
label: this.causeLabel(item.cause),
|
|
48889
|
+
}));
|
|
48890
|
+
}
|
|
48891
|
+
/** What asked for a refresh, on one line. */
|
|
48892
|
+
causeLabel(cause) {
|
|
48893
|
+
const scope = cause.url ? this.requestPath(cause.url) : 'everything in use';
|
|
48894
|
+
const what = cause.type === 'invalidation'
|
|
48895
|
+
? `invalidated ${scope}`
|
|
48896
|
+
: cause.type === 'mutation'
|
|
48897
|
+
? `mutation on ${scope}`
|
|
48898
|
+
: `refreshed ${scope}`;
|
|
48899
|
+
return cause.otherTab ? `${what} · another tab` : what;
|
|
48900
|
+
}
|
|
47909
48901
|
/** Derives the per-step status of a sequence step from its live progress signals. */
|
|
47910
48902
|
sequenceStepStatus(sequence, index) {
|
|
47911
48903
|
const failedAt = sequence.failedAt();
|
|
@@ -47927,25 +48919,12 @@ class QueryDevtoolsComponent {
|
|
|
47927
48919
|
queriesForStack(stack) {
|
|
47928
48920
|
const inner = stack.queries();
|
|
47929
48921
|
const queryEntries = this.queryEntries();
|
|
47930
|
-
return inner.map((query) =>
|
|
47931
|
-
const entry = queryEntries.find((e) => e.handle === query);
|
|
47932
|
-
return {
|
|
47933
|
-
id: entry?.id ?? '',
|
|
47934
|
-
query: query,
|
|
47935
|
-
method: entry?.meta.method ?? '',
|
|
47936
|
-
route: entry?.meta.route ?? '',
|
|
47937
|
-
clientName: entry?.meta.clientName ?? '',
|
|
47938
|
-
clientBaseUrl: entry?.meta.clientBaseUrl ?? '',
|
|
47939
|
-
};
|
|
47940
|
-
});
|
|
48922
|
+
return inner.map((query) => this.queryLinkFor(queryEntries.find((e) => e.handle === query), query));
|
|
47941
48923
|
}
|
|
47942
48924
|
/** Identifying info for a stack, derived from its (uniform) inner queries. */
|
|
47943
48925
|
stackIdentity(stack) {
|
|
47944
48926
|
const first = this.queriesForStack(stack)[0];
|
|
47945
|
-
return { method: first?.method ?? '',
|
|
47946
|
-
}
|
|
47947
|
-
authFeatures(auth) {
|
|
47948
|
-
return Object.keys(auth.features ?? {});
|
|
48927
|
+
return { method: first?.method ?? '', segments: first?.segments ?? [], baseUrl: first?.clientBaseUrl ?? '' };
|
|
47949
48928
|
}
|
|
47950
48929
|
authQueryKeys(auth) {
|
|
47951
48930
|
return Object.keys(auth.queries ?? {});
|
|
@@ -47968,15 +48947,7 @@ class QueryDevtoolsComponent {
|
|
|
47968
48947
|
}
|
|
47969
48948
|
queriesForSequence(sequence) {
|
|
47970
48949
|
const queryEntries = this.queryEntries();
|
|
47971
|
-
return sequence.queries.map((query) =>
|
|
47972
|
-
const entry = queryEntries.find((e) => e.handle === query);
|
|
47973
|
-
return {
|
|
47974
|
-
id: entry?.id ?? '',
|
|
47975
|
-
query: query,
|
|
47976
|
-
method: entry?.meta.method ?? '',
|
|
47977
|
-
route: entry?.meta.route ?? '',
|
|
47978
|
-
};
|
|
47979
|
-
});
|
|
48950
|
+
return sequence.queries.map((query) => this.queryLinkFor(queryEntries.find((e) => e.handle === query), query));
|
|
47980
48951
|
}
|
|
47981
48952
|
/** The snapshot of a sequence step, once it has run (holds the args in and the response/error out). */
|
|
47982
48953
|
stepSnapshot(sequence, index) {
|
|
@@ -48013,6 +48984,16 @@ class QueryDevtoolsComponent {
|
|
|
48013
48984
|
.replace(/_/g, ' ')
|
|
48014
48985
|
.toLowerCase();
|
|
48015
48986
|
}
|
|
48987
|
+
/** A feature and its options on one line, for a report or a chip's tooltip. */
|
|
48988
|
+
featureSummary(feature) {
|
|
48989
|
+
const details = feature.details.map((detail) => `${detail.label} ${detail.value}`);
|
|
48990
|
+
return [this.featureLabel(feature.type), ...details].join(' · ');
|
|
48991
|
+
}
|
|
48992
|
+
/** The features of the client behind a cache tab card, or `null` for a client without any. */
|
|
48993
|
+
clientFeatures(client) {
|
|
48994
|
+
const features = client?.subtle.devtoolsFeatures ?? [];
|
|
48995
|
+
return features.length ? features : null;
|
|
48996
|
+
}
|
|
48016
48997
|
formatTime(timestamp) {
|
|
48017
48998
|
if (!timestamp)
|
|
48018
48999
|
return '-';
|
|
@@ -48021,8 +49002,185 @@ class QueryDevtoolsComponent {
|
|
|
48021
49002
|
eventTypeLabel(event) {
|
|
48022
49003
|
if (event.type === 'unbind-all-secure')
|
|
48023
49004
|
return 'logout';
|
|
49005
|
+
if (event.type === 'queries-refreshed')
|
|
49006
|
+
return `refetch ×${event.refreshed?.length ?? 0}`;
|
|
48024
49007
|
return event.type === 'request-error' ? `error ${event.status}` : 'success';
|
|
48025
49008
|
}
|
|
49009
|
+
/** A registered query as a row that opens the detail drawer. */
|
|
49010
|
+
queryLinkFor(entry, query = entry?.handle) {
|
|
49011
|
+
return {
|
|
49012
|
+
id: entry?.id ?? '',
|
|
49013
|
+
query,
|
|
49014
|
+
method: entry?.meta.method ?? '',
|
|
49015
|
+
segments: this.routeSegments(entry, query),
|
|
49016
|
+
clientBaseUrl: entry?.meta.clientBaseUrl ?? '',
|
|
49017
|
+
stats: entry?.stats,
|
|
49018
|
+
};
|
|
49019
|
+
}
|
|
49020
|
+
/** The activity of one entry, or the total of a group of them (a stack's queries, a whole tab). */
|
|
49021
|
+
activityOf(handles) {
|
|
49022
|
+
const stats = sumQueryDevtoolsStats(handles);
|
|
49023
|
+
return {
|
|
49024
|
+
stats,
|
|
49025
|
+
cacheServed: Math.max(0, stats.executions - stats.requests),
|
|
49026
|
+
avgDurationMs: stats.responses ? Math.round(stats.totalDurationMs / stats.responses) : null,
|
|
49027
|
+
avgResponseBytes: stats.responses ? Math.round(stats.receivedBytes / stats.responses) : null,
|
|
49028
|
+
hasActivity: stats.executions > 0,
|
|
49029
|
+
};
|
|
49030
|
+
}
|
|
49031
|
+
/** An activity summary on one line, for a report. `null` for a query that has not run. */
|
|
49032
|
+
activitySummary(activity) {
|
|
49033
|
+
if (!activity.hasActivity)
|
|
49034
|
+
return null;
|
|
49035
|
+
const { stats } = activity;
|
|
49036
|
+
const parts = [
|
|
49037
|
+
`${stats.executions} execution${stats.executions === 1 ? '' : 's'}`,
|
|
49038
|
+
`${stats.requests} request${stats.requests === 1 ? '' : 's'}`,
|
|
49039
|
+
`↓ ${this.formatTransferred(stats.receivedBytes, stats.hasEstimatedBytes)}`,
|
|
49040
|
+
];
|
|
49041
|
+
if (stats.sentBytes)
|
|
49042
|
+
parts.push(`↑ ${this.formatTransferred(stats.sentBytes, stats.hasEstimatedBytes)}`);
|
|
49043
|
+
if (stats.errors)
|
|
49044
|
+
parts.push(`${stats.errors} failed`);
|
|
49045
|
+
if (stats.retries)
|
|
49046
|
+
parts.push(`${stats.retries} retr${stats.retries === 1 ? 'y' : 'ies'}`);
|
|
49047
|
+
if (activity.avgDurationMs !== null)
|
|
49048
|
+
parts.push(`avg ${this.formatDuration(activity.avgDurationMs)}`);
|
|
49049
|
+
return `activity: ${parts.join(' · ')}`;
|
|
49050
|
+
}
|
|
49051
|
+
/** A query matches the chips if it is in any of the picked states - chips widen, they don't intersect. */
|
|
49052
|
+
matchesFacets(query, facets) {
|
|
49053
|
+
const status = this.queryStatus(query);
|
|
49054
|
+
return ((facets.has('error') && status === 'error') ||
|
|
49055
|
+
(facets.has('loading') && status === 'loading') ||
|
|
49056
|
+
(facets.has('idle') && status === 'idle') ||
|
|
49057
|
+
(facets.has('stale') && this.isStale(query)));
|
|
49058
|
+
}
|
|
49059
|
+
/**
|
|
49060
|
+
* What the search box matches against: what the row shows, plus the path of the request that ran for a
|
|
49061
|
+
* query whose template differs from it.
|
|
49062
|
+
*
|
|
49063
|
+
* Deliberately not the origin or the client name - both repeat across most entries, so a short term
|
|
49064
|
+
* ("p") would match nearly everything through the host name. Scoping to a client is the picker's job.
|
|
49065
|
+
*/
|
|
49066
|
+
queryHaystack(entry, query) {
|
|
49067
|
+
const url = this.requestUrl(query);
|
|
49068
|
+
const parts = [entry.meta.method, this.queryRoute(entry, query), url ? this.requestPath(url) : null];
|
|
49069
|
+
return parts.join(' ').toLowerCase();
|
|
49070
|
+
}
|
|
49071
|
+
/** {@link routeSegments} as a plain string, for the places that cannot render markup. */
|
|
49072
|
+
queryRoute(entry, query) {
|
|
49073
|
+
return this.routeSegments(entry, query)
|
|
49074
|
+
.map((segment) => segment.text)
|
|
49075
|
+
.join('');
|
|
49076
|
+
}
|
|
49077
|
+
/**
|
|
49078
|
+
* Describes a query the way a replay outside the app needs it: the URL, headers and body of the
|
|
49079
|
+
* request it last made, or - for a query that has not run - what its current args would send.
|
|
49080
|
+
*/
|
|
49081
|
+
insomniaRequest(entry, query) {
|
|
49082
|
+
const request = query.subtle.request();
|
|
49083
|
+
const args = this.queryArgs(query);
|
|
49084
|
+
const route = this.queryRoute(entry, query);
|
|
49085
|
+
return {
|
|
49086
|
+
// A GraphQL query has no HTTP method of its own until it runs; its transport is POST unless the
|
|
49087
|
+
// creator says otherwise, which is only knowable from the request.
|
|
49088
|
+
method: request?.method ?? (entry.meta.gqlQuery ? 'POST' : (entry.meta.method ?? 'GET')),
|
|
49089
|
+
url: request?.url ?? `${entry.meta.clientBaseUrl ?? ''}${route}`,
|
|
49090
|
+
// Query params are part of the name so that several requests to the same endpoint (the pages of a
|
|
49091
|
+
// stack, a search) stay tellable apart in Insomnia's sidebar.
|
|
49092
|
+
name: `${entry.meta.method ?? ''} ${request ? this.requestPath(request.url) : route || query.id() || 'request'}`.trim(),
|
|
49093
|
+
headers: this.insomniaHeaders(request, args),
|
|
49094
|
+
body: args?.body ?? null,
|
|
49095
|
+
gqlQuery: entry.meta.gqlQuery ? this.gqlDocument(entry.meta.gqlQuery) : null,
|
|
49096
|
+
group: entry.meta.clientName ?? null,
|
|
49097
|
+
secureBy: entry.meta.isSecure ? (entry.meta.authProviderName ?? null) : null,
|
|
49098
|
+
};
|
|
49099
|
+
}
|
|
49100
|
+
/**
|
|
49101
|
+
* The token refreshes the given requests authenticate with - one per auth provider they name, and
|
|
49102
|
+
* only for a provider that is logged in, since a refresh request without a refresh token has
|
|
49103
|
+
* nothing to send.
|
|
49104
|
+
*/
|
|
49105
|
+
insomniaTokenRefreshes(requests) {
|
|
49106
|
+
const names = new Set(requests.map((request) => request.secureBy).filter((name) => typeof name === 'string'));
|
|
49107
|
+
return Array.from(names)
|
|
49108
|
+
.map((name) => this.insomniaTokenRefresh(name))
|
|
49109
|
+
.filter((refresh) => refresh !== null);
|
|
49110
|
+
}
|
|
49111
|
+
insomniaTokenRefresh(providerName) {
|
|
49112
|
+
const entry = this.authEntries().find((candidate) => candidate.meta.name === providerName);
|
|
49113
|
+
const refresh = entry?.meta.authQueries?.find((authQuery) => authQuery.kind === 'token-refresh');
|
|
49114
|
+
if (!entry || !refresh)
|
|
49115
|
+
return null;
|
|
49116
|
+
const provider = entry.handle;
|
|
49117
|
+
const refreshToken = provider.refreshToken();
|
|
49118
|
+
if (!refreshToken)
|
|
49119
|
+
return null;
|
|
49120
|
+
return {
|
|
49121
|
+
id: providerName,
|
|
49122
|
+
name: `${refresh.method} ${refresh.route} (token refresh)`,
|
|
49123
|
+
method: refresh.method,
|
|
49124
|
+
url: `${entry.meta.clientBaseUrl ?? ''}${refresh.route}`,
|
|
49125
|
+
headers: [],
|
|
49126
|
+
body: refresh.buildArgs?.(refreshToken).body ?? null,
|
|
49127
|
+
group: entry.meta.clientName ?? null,
|
|
49128
|
+
accessTokenPath: this.accessTokenPath(provider),
|
|
49129
|
+
maxAgeSeconds: this.accessTokenMaxAge(provider),
|
|
49130
|
+
};
|
|
49131
|
+
}
|
|
49132
|
+
/**
|
|
49133
|
+
* Where the access token sits in the refresh response. A provider's `extractTokens` can pull it out
|
|
49134
|
+
* of any shape, so the path is recovered by finding the live token in the last auth response - with
|
|
49135
|
+
* the default extractor's `$.accessToken` as the fallback.
|
|
49136
|
+
*/
|
|
49137
|
+
accessTokenPath(provider) {
|
|
49138
|
+
const token = provider.accessToken();
|
|
49139
|
+
const response = provider.latestExecutedQuery()?.snapshot.response();
|
|
49140
|
+
const path = token && response ? findValuePath(token, { value: response, path: '$', depth: 0 }) : null;
|
|
49141
|
+
return path ?? '$.accessToken';
|
|
49142
|
+
}
|
|
49143
|
+
/**
|
|
49144
|
+
* How long Insomnia may reuse a stored refresh response: the access token's own lifetime with a
|
|
49145
|
+
* margin, so the chain refreshes shortly before the token it hands out would expire. Capped at an
|
|
49146
|
+
* hour - a long-lived (or bogus) `exp` should still hand out a token minted this session.
|
|
49147
|
+
*/
|
|
49148
|
+
accessTokenMaxAge(provider) {
|
|
49149
|
+
const payload = decodeJwtPayload(provider.accessToken());
|
|
49150
|
+
const exp = payload?.['exp'];
|
|
49151
|
+
const iat = payload?.['iat'];
|
|
49152
|
+
if (typeof exp !== 'number')
|
|
49153
|
+
return DEFAULT_TOKEN_MAX_AGE_S;
|
|
49154
|
+
const lifetime = exp - (typeof iat === 'number' ? iat : Math.floor(Date.now() / 1000));
|
|
49155
|
+
if (lifetime <= 0)
|
|
49156
|
+
return DEFAULT_TOKEN_MAX_AGE_S;
|
|
49157
|
+
return Math.min(MAX_TOKEN_MAX_AGE_S, Math.max(60, Math.round(lifetime * 0.9)));
|
|
49158
|
+
}
|
|
49159
|
+
/**
|
|
49160
|
+
* The headers a replay needs, including the ones the query client adds. Header providers can throw
|
|
49161
|
+
* (a secure query's needs an access token), in which case the request is exported without them.
|
|
49162
|
+
*/
|
|
49163
|
+
insomniaHeaders(request, args) {
|
|
49164
|
+
try {
|
|
49165
|
+
const headers = request
|
|
49166
|
+
? request.subtle.resolveHeaders()
|
|
49167
|
+
: typeof args?.headers === 'function'
|
|
49168
|
+
? args.headers()
|
|
49169
|
+
: args?.headers;
|
|
49170
|
+
return (headers?.keys() ?? []).map((name) => ({ name, value: headers?.getAll(name)?.join(', ') ?? '' }));
|
|
49171
|
+
}
|
|
49172
|
+
catch {
|
|
49173
|
+
return [];
|
|
49174
|
+
}
|
|
49175
|
+
}
|
|
49176
|
+
downloadFile(fileName, content) {
|
|
49177
|
+
const url = URL.createObjectURL(new Blob([content], { type: 'application/json' }));
|
|
49178
|
+
const anchor = this.renderer.createElement('a');
|
|
49179
|
+
anchor.href = url;
|
|
49180
|
+
anchor.download = fileName;
|
|
49181
|
+
anchor.click();
|
|
49182
|
+
URL.revokeObjectURL(url);
|
|
49183
|
+
}
|
|
48026
49184
|
stepKey(entryId, index) {
|
|
48027
49185
|
return `${entryId}:${index}`;
|
|
48028
49186
|
}
|
|
@@ -48069,31 +49227,37 @@ class QueryDevtoolsComponent {
|
|
|
48069
49227
|
this.inspectActive.set(false);
|
|
48070
49228
|
}
|
|
48071
49229
|
selectionKey() {
|
|
48072
|
-
return
|
|
48073
|
-
|
|
48074
|
-
|
|
49230
|
+
return [
|
|
49231
|
+
this.selectedQueryId(),
|
|
49232
|
+
this.stackSelectedQueryId(),
|
|
49233
|
+
this.sequenceSelectedQueryId(),
|
|
49234
|
+
this.formSelectedQueryId(),
|
|
49235
|
+
this.timelineSelectedQueryId(),
|
|
49236
|
+
].join('|');
|
|
49237
|
+
}
|
|
49238
|
+
/** Writes to the clipboard and ticks `copied` on success. `html` is omitted for plain-text payloads. */
|
|
49239
|
+
writeToClipboard(payload, copied) {
|
|
48075
49240
|
const clipboard = navigator.clipboard;
|
|
49241
|
+
const { text, html } = payload;
|
|
48076
49242
|
if (!clipboard)
|
|
48077
49243
|
return;
|
|
49244
|
+
const flag = () => {
|
|
49245
|
+
copied.set(true);
|
|
49246
|
+
this.copiedReset$.next();
|
|
49247
|
+
};
|
|
48078
49248
|
// Prefer rich HTML (Slack keeps the formatting on paste); fall back to plain text.
|
|
48079
|
-
if ('write' in clipboard && typeof ClipboardItem !== 'undefined') {
|
|
49249
|
+
if (html !== undefined && 'write' in clipboard && typeof ClipboardItem !== 'undefined') {
|
|
48080
49250
|
const item = new ClipboardItem({
|
|
48081
49251
|
'text/html': new Blob([html], { type: 'text/html' }),
|
|
48082
49252
|
'text/plain': new Blob([text], { type: 'text/plain' }),
|
|
48083
49253
|
});
|
|
48084
49254
|
clipboard
|
|
48085
49255
|
.write([item])
|
|
48086
|
-
.then(
|
|
48087
|
-
.catch(() => clipboard
|
|
48088
|
-
.writeText(text)
|
|
48089
|
-
.then(() => this.copiedReport.set(true))
|
|
48090
|
-
.catch(() => undefined));
|
|
49256
|
+
.then(flag)
|
|
49257
|
+
.catch(() => clipboard.writeText(text).then(flag).catch(noop));
|
|
48091
49258
|
return;
|
|
48092
49259
|
}
|
|
48093
|
-
clipboard
|
|
48094
|
-
.writeText(text)
|
|
48095
|
-
.then(() => this.copiedReport.set(true))
|
|
48096
|
-
.catch(() => undefined);
|
|
49260
|
+
clipboard.writeText(text).then(flag).catch(noop);
|
|
48097
49261
|
}
|
|
48098
49262
|
responseStatus(query) {
|
|
48099
49263
|
const event = query.latestHttpEvent();
|
|
@@ -48110,28 +49274,66 @@ class QueryDevtoolsComponent {
|
|
|
48110
49274
|
// so a row for it would only ever duplicate the next one.
|
|
48111
49275
|
if (event.type === 'entry-created')
|
|
48112
49276
|
return;
|
|
48113
|
-
const base = {
|
|
49277
|
+
const base = {
|
|
49278
|
+
id: this.eventIdCounter++,
|
|
49279
|
+
timestamp: Date.now(),
|
|
49280
|
+
client,
|
|
49281
|
+
type: event.type,
|
|
49282
|
+
cause: null,
|
|
49283
|
+
refreshed: null,
|
|
49284
|
+
};
|
|
48114
49285
|
// A logout drops every secure entry at once - worth a row of its own, since the requests that
|
|
48115
49286
|
// disappear from the cache view are otherwise unexplained.
|
|
48116
|
-
|
|
48117
|
-
|
|
48118
|
-
|
|
49287
|
+
if (event.type === 'unbind-all-secure') {
|
|
49288
|
+
this.pushEventItem({ ...base, method: null, url: null, isSecure: true, status: null, queryId: null });
|
|
49289
|
+
return;
|
|
49290
|
+
}
|
|
49291
|
+
// An invalidation is one row for the whole fan-out: the six queries it re-executed are listed on it
|
|
49292
|
+
// rather than blurring into the six request rows that follow.
|
|
49293
|
+
if (event.type === 'queries-refreshed') {
|
|
49294
|
+
this.pushEventItem({
|
|
48119
49295
|
...base,
|
|
48120
|
-
method:
|
|
48121
|
-
url:
|
|
48122
|
-
isSecure:
|
|
48123
|
-
status:
|
|
48124
|
-
|
|
49296
|
+
method: null,
|
|
49297
|
+
url: null,
|
|
49298
|
+
isSecure: false,
|
|
49299
|
+
status: null,
|
|
49300
|
+
queryId: null,
|
|
49301
|
+
cause: event.cause,
|
|
49302
|
+
refreshed: event.requests.map((request) => this.refreshedRequestOf(request)),
|
|
49303
|
+
});
|
|
49304
|
+
return;
|
|
49305
|
+
}
|
|
49306
|
+
this.pushEventItem({
|
|
49307
|
+
...base,
|
|
49308
|
+
method: event.request.method,
|
|
49309
|
+
url: event.request.url,
|
|
49310
|
+
isSecure: event.isSecure,
|
|
49311
|
+
status: event.type === 'request-error' ? event.error.status : null,
|
|
49312
|
+
// A request is shared by every query on the same cache key, so the first owner is as good
|
|
49313
|
+
// as any - they all show the same response.
|
|
49314
|
+
queryId: this.queryEntries().find((e) => e.handle.subtle.request() === event.request)?.id ?? null,
|
|
49315
|
+
});
|
|
49316
|
+
}
|
|
49317
|
+
refreshedRequestOf(request) {
|
|
49318
|
+
return {
|
|
49319
|
+
queryIds: this.queryEntries()
|
|
49320
|
+
.filter((e) => e.handle.subtle.request() === request)
|
|
49321
|
+
.map((owner) => owner.id),
|
|
49322
|
+
method: request.method,
|
|
49323
|
+
path: this.requestPath(request.url),
|
|
49324
|
+
};
|
|
49325
|
+
}
|
|
49326
|
+
pushEventItem(item) {
|
|
48125
49327
|
this.eventLog.update((log) => [item, ...log].slice(0, MAX_EVENTS));
|
|
48126
49328
|
}
|
|
48127
49329
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: QueryDevtoolsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
48128
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: QueryDevtoolsComponent, isStandalone: true, selector: "et-query-devtools", host: { classAttribute: "et-query-devtools-host" }, ngImport: i0, template: "@if (open()) {\n <section\n [class.et-query-devtools-panel--resizing]=\"resizing()\"\n [style.height.px]=\"panelHeight()\"\n class=\"et-query-devtools-panel\"\n >\n <div\n (pointerdown)=\"startResize($event)\"\n class=\"et-query-devtools-resize\"\n title=\"Drag to resize\"\n aria-hidden=\"true\"\n ></div>\n <header class=\"et-query-devtools-header\">\n <nav class=\"et-query-devtools-tabs\" role=\"tablist\">\n @for (tab of tabs; track tab.id) {\n <button\n [class.et-query-devtools-tab--active]=\"activeTab() === tab.id\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n (click)=\"activeTab.set(tab.id)\"\n class=\"et-query-devtools-tab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n </button>\n }\n </nav>\n\n <div class=\"et-query-devtools-header-actions\">\n <button\n [class.et-query-devtools-inspect-btn--active]=\"inspectActive()\"\n (click)=\"toggleInspect()\"\n class=\"et-query-devtools-inspect-btn\"\n type=\"button\"\n title=\"Inspect: hover the UI to find its query\"\n >\n \u2316 Inspect\n </button>\n <button\n (click)=\"toggleOpen()\"\n class=\"et-query-devtools-close-panel\"\n type=\"button\"\n title=\"Close devtools ({{ shortcut }})\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Close\n <kbd class=\"et-query-devtools-kbd\">{{ shortcut }}</kbd>\n </button>\n </div>\n </header>\n\n <div class=\"et-query-devtools-body\">\n @switch (activeTab()) {\n <!-- QUERIES -->\n @case ('queries') {\n <div class=\"et-query-devtools-master-detail\">\n <div class=\"et-query-devtools-list\">\n <div [class.et-query-devtools-toolbar--filtered]=\"!!inspectFilterIds()\" class=\"et-query-devtools-toolbar\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n <button\n (click)=\"clearInspectFilter()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the inspection filter and show all queries\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Clear\n </button>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #clientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(clientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n <span class=\"et-query-devtools-count\">{{ filteredQueries().length }}</span>\n }\n </div>\n\n @for (item of filteredQueries(); track item.entry.id) {\n <button\n [class.et-query-devtools-row--active]=\"selectedQueryId() === item.entry.id\"\n (click)=\"selectedQueryId.set(item.entry.id)\"\n class=\"et-query-devtools-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"queryStatus(item.query)\"\n [title]=\"queryStatus(item.query)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ item.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">{{ item.entry.meta.route || '\u2014' }}</span>\n @if (isStale(item.query)) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">stale</span>\n }\n </button>\n } @empty {\n @if (inspectFilterIds()) {\n <p class=\"et-query-devtools-empty\">\n The inspected element's queries are no longer registered. Clear the filter.\n </p>\n } @else {\n <p class=\"et-query-devtools-empty\">No queries registered.</p>\n }\n }\n </div>\n\n <div class=\"et-query-devtools-detail\">\n @if (selectedQuery(); as sel) {\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Select a query to inspect it.</p>\n }\n </div>\n </div>\n }\n\n <!-- STACKS -->\n @case ('stacks') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of stackEntries(); track entry.id) {\n @if (entry.kind === 'paged-query-stack') {\n @let paged = asPagedStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">paged stack</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ paged.direction() }}</span>\n @if (paged.loading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n </div>\n\n @let pagedId = stackIdentity(paged);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ pagedId.method }}</span>\n <span class=\"et-query-devtools-route\">{{ pagedId.route }}</span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ pagedId.baseUrl }}</div>\n @if (entry.meta.featureTypes?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (feature of entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Pages loaded</dt>\n <dd>{{ paged.queries().length }}</dd>\n <dt>Items</dt>\n <dd>{{ paged.items().length }}</dd>\n <dt>First page loaded</dt>\n <dd>{{ paged.isFirstPageLoaded() }}</dd>\n <dt>Last page loaded</dt>\n <dd>{{ paged.isLastPageLoaded() }}</dd>\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(paged) }\" />\n </div>\n } @else {\n @let stack = asStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">stack</span>\n @if (stack.anyLoading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n @if (stack.anyError()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">error</span>\n }\n </div>\n\n @let stackId = stackIdentity(stack);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ stackId.method }}</span>\n <span class=\"et-query-devtools-route\">{{ stackId.route }}</span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ stackId.baseUrl }}</div>\n @if (entry.meta.featureTypes?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (feature of entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Queries</dt>\n <dd>{{ stack.queries().length }}</dd>\n <dt>Loading</dt>\n <dd>{{ stack.loadingProgress().loaded }} / {{ stack.loadingProgress().total }}</dd>\n <dt>Errors</dt>\n <dd>{{ stack.errors().length }}</dd>\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(stack) }\" />\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No stacks registered.</p>\n }\n </div>\n\n @if (stackSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"stackSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- SEQUENCES -->\n @case ('sequences') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of sequenceEntries(); track entry.id) {\n @let sequence = asSequence(entry);\n @if (sequence) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">sequence</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ sequence.status() }}</span>\n <span class=\"et-query-devtools-count\"\n >step {{ sequence.currentStep() }} / {{ sequence.total }}</span\n >\n </div>\n\n <ol class=\"et-query-devtools-step-list\">\n @for (link of queriesForSequence(sequence); track $index; let i = $index) {\n <li [attr.data-status]=\"sequenceStepStatus(sequence, i)\" class=\"et-query-devtools-step-item\">\n <div class=\"et-query-devtools-step-head\">\n <button\n [class.et-query-devtools-inner-row--active]=\"sequenceSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"sequenceSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"sequenceStepStatus(sequence, i)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ i + 1 }}. {{ link.method }}</span>\n <span class=\"et-query-devtools-route\">{{ link.route || 'step ' + (i + 1) }}</span>\n </button>\n <button\n [attr.aria-expanded]=\"isStepExpanded(entry.id, i)\"\n (click)=\"toggleStep(entry.id, i)\"\n class=\"et-query-devtools-step-caret-btn\"\n type=\"button\"\n title=\"Toggle in / out\"\n >\n {{ isStepExpanded(entry.id, i) ? '\u25BE' : '\u25B8' }}\n </button>\n </div>\n\n @if (isStepExpanded(entry.id, i)) {\n @let snap = stepSnapshot(sequence, i);\n <div class=\"et-query-devtools-step-io\">\n <h5 class=\"et-query-devtools-section\">In \u00B7 args</h5>\n @if (sequence.stepArgs().length > i) {\n <et-query-devtools-json [value]=\"sequence.stepArgs()[i]\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n\n @if (snap?.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 error ({{ error.raw.status }})</h5>\n <et-query-devtools-json [value]=\"error.isList ? error.errors : error.error\" />\n } @else {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 response</h5>\n @if (snap) {\n <et-query-devtools-json [value]=\"snap.response()\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n }\n </div>\n }\n </li>\n }\n </ol>\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No sequences registered.</p>\n }\n </div>\n\n @if (sequenceSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"sequenceSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- AUTH -->\n @case ('auth') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of authEntries(); track entry.id) {\n @let auth = asAuth(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'auth provider' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"auth.isAuthenticated()\"\n [class.et-query-devtools-chip--muted]=\"!auth.isAuthenticated()\"\n class=\"et-query-devtools-chip\"\n >\n {{ auth.isAuthenticated() ? 'authenticated' : 'anonymous' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.clientBaseUrl || entry.meta.clientName || '\u2014' }}</dd>\n <dt>Access token</dt>\n <dd>{{ auth.accessToken() ? 'present' : 'none' }}</dd>\n <dt>Refresh token</dt>\n <dd>{{ auth.refreshToken() ? 'present' : 'none' }}</dd>\n @if (authTokenExpiry(auth); as expiry) {\n <dt>Token expires in</dt>\n <dd>{{ expiry }}</dd>\n }\n @if (auth.executionState(); as state) {\n <dt>Execution state</dt>\n <dd>{{ state.type }} \u00B7 {{ state.state }}</dd>\n }\n @if (authQueryKeys(auth).length) {\n <dt>Queries</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (key of authQueryKeys(auth); track key) {\n <span class=\"et-query-devtools-chip\">{{ key }}</span>\n }\n </div>\n </dd>\n }\n @if (authFeatures(auth).length) {\n <dt>Features</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (feature of authFeatures(auth); track feature) {\n <span class=\"et-query-devtools-chip\">{{ feature }}</span>\n }\n </div>\n </dd>\n }\n </dl>\n\n @if (authTokenPayload(auth); as payload) {\n <h4 class=\"et-query-devtools-section\">Access token payload</h4>\n <et-query-devtools-json [value]=\"payload\" />\n }\n\n @if (auth.latestNonInternalQuery(); as latest) {\n <h4 class=\"et-query-devtools-section\">Latest auth query \u00B7 {{ latest.key }}</h4>\n <et-query-devtools-json [value]=\"latest.snapshot.response()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No auth providers registered.</p>\n }\n </div>\n }\n\n <!-- SOCKETS -->\n @case ('ws') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of wsEntries(); track entry.id) {\n @let ws = asWs(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'socket' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"ws.connected()\"\n [class.et-query-devtools-chip--muted]=\"!ws.connected()\"\n class=\"et-query-devtools-chip\"\n >\n {{ ws.connected() ? 'connected' : 'disconnected' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.url }}</dd>\n <dt>Rooms</dt>\n <dd>\n @if (ws.rooms().length) {\n <div class=\"et-query-devtools-chips\">\n @for (room of ws.rooms(); track room) {\n <span class=\"et-query-devtools-chip\">{{ room }}</span>\n }\n </div>\n } @else {\n none\n }\n </dd>\n </dl>\n\n <h5 class=\"et-query-devtools-section\">Messages ({{ ws.messages().length }})</h5>\n @for (message of ws.messages(); track message.id) {\n <div class=\"et-query-devtools-ws-message\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-count\">{{ formatTime(message.timestamp) }}</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ message.room }}</span>\n <span class=\"et-query-devtools-method\">{{ message.event }}</span>\n </div>\n <et-query-devtools-json [value]=\"message.data\" />\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No messages received yet.</p>\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No web socket clients registered.</p>\n }\n </div>\n }\n\n <!-- CACHE -->\n @case ('cache') {\n <div class=\"et-query-devtools-scroll\">\n @for (client of cacheView(); track client.name) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.baseUrl || client.name }}</strong>\n <span class=\"et-query-devtools-count\">{{ client.entries.length }} entries</span>\n @if (client.client?.subtle?.persistence) {\n <span class=\"et-query-devtools-count\">{{ persistedCount(client.client!) }} on disk</span>\n <button (click)=\"clearPersistedQueries(client.client!)\" class=\"et-query-devtools-btn\" type=\"button\">\n Clear disk\n </button>\n }\n </div>\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Request</th>\n <th>Consumers</th>\n <th>Secure</th>\n <th>Freshness</th>\n <th>Sync</th>\n <th>Disk</th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (cacheEntry of client.entries; track cacheEntry.key) {\n <tr>\n <td [title]=\"cacheEntry.request.method + ' ' + cacheEntry.request.url\">\n <span class=\"et-query-devtools-method\">{{ cacheEntry.request.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(cacheEntry.request.url) }}</span>\n </td>\n <td>{{ cacheEntry.consumerCount }}</td>\n <td>{{ cacheEntry.isSecure ? 'yes' : 'no' }}</td>\n <td>{{ cacheFreshness(cacheEntry) }}</td>\n <td>{{ cacheSync(cacheEntry, client.pollStates) }}</td>\n <td>{{ cachePersistence(cacheEntry) }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n <button (click)=\"refetchCacheEntry(cacheEntry)\" class=\"et-query-devtools-btn\" type=\"button\">\n Refetch\n </button>\n <button\n (click)=\"evictCacheEntry(client.repository, cacheEntry.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Evict\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"7\">Cache empty.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No clients with a cache yet.</p>\n }\n </div>\n }\n\n <!-- EVENTS -->\n @case ('events') {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-toolbar\">\n <span class=\"et-query-devtools-count\">{{ eventLog().length }} events</span>\n <button (click)=\"clearEvents()\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Base URL</th>\n <th>Type</th>\n <th>Request</th>\n </tr>\n </thead>\n <tbody>\n @for (event of eventLog(); track event.id) {\n <tr [attr.data-status]=\"event.type === 'request-error' ? 'error' : 'success'\">\n <td>{{ formatTime(event.timestamp) }}</td>\n <td class=\"et-query-devtools-mono\">{{ event.client }}</td>\n <td>\n {{ eventTypeLabel(event) }}\n @if (event.isSecure) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">secure</span>\n }\n </td>\n @if (event.url; as url) {\n <td [title]=\"event.method + ' ' + url\">\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </td>\n } @else {\n <td class=\"et-query-devtools-url\">all secure entries dropped</td>\n }\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"4\">No events captured yet.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n </div>\n\n <!-- Reusable inner-query list (stacks) \u2014 clicking opens the split-view drawer, not a tab switch. -->\n <ng-template #innerQueriesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track $index) {\n <button\n [class.et-query-devtools-inner-row--active]=\"stackSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"stackSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">{{ link.route || link.query.id() || 'query ' + ($index + 1) }}</span>\n </button>\n }\n </div>\n </ng-template>\n\n <!-- Reusable query detail \u2014 used by the Queries tab and the stacks split-view drawer. -->\n <ng-template #queryDetailTpl let-sel=\"sel\">\n <div class=\"et-query-devtools-detail-head\">\n <span class=\"et-query-devtools-method\">{{ sel.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">{{ sel.entry.meta.route || '\u2014' }}</span>\n </div>\n\n <div class=\"et-query-devtools-actions\">\n <span class=\"et-query-devtools-actions-label\">Run</span>\n <button (click)=\"executeQuery(sel.query, false)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"executeQuery(sel.query, true)\" class=\"et-query-devtools-btn\" type=\"button\">Cached</button>\n <button (click)=\"resetQuery(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Reset</button>\n <button (click)=\"copyReport(sel.entry, sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">\n {{ copiedReport() ? '\u2713 Copied' : 'Copy report' }}\n </button>\n <span class=\"et-query-devtools-actions-label\">Edit</span>\n <button (click)=\"openResponseEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Response</button>\n <button (click)=\"openArgsEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Replay args</button>\n <span class=\"et-query-devtools-actions-label\">Force</span>\n <button (click)=\"forceLoading(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Loading</button>\n <button (click)=\"forceError(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Error</button>\n <button (click)=\"forceEmpty(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Empty</button>\n <button (click)=\"clearForced(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n\n @if (editorMode() !== 'none') {\n <div class=\"et-query-devtools-editor\">\n @if (editorMode() === 'response') {\n <h4 class=\"et-query-devtools-section\">Edit response \u2192 setResponse()</h4>\n <textarea\n #respArea\n [value]=\"responseDraft()\"\n (input)=\"responseDraft.set(respArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyResponse(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Apply</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n } @else {\n <h4 class=\"et-query-devtools-section\">Replay with edited args \u2192 execute()</h4>\n <textarea\n #argsArea\n [value]=\"argsDraft()\"\n (input)=\"argsDraft.set(argsArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyArgs(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n }\n @if (editError(); as err) {\n <p class=\"et-query-devtools-editor-error\">{{ err }}</p>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.entry.meta.clientBaseUrl || sel.entry.meta.clientName || '\u2014' }}</dd>\n <dt>Status</dt>\n <dd>{{ queryStatus(sel.query) }}</dd>\n <dt>Cache key</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.query.id() || '\u2014' }}</dd>\n <dt>Last executed</dt>\n <dd>{{ formatTime(sel.query.lastTimeExecutedAt()) }}</dd>\n <dt>Triggered by</dt>\n <dd>{{ sel.query.triggeredBy() || 'user' }}</dd>\n @if (sel.entry.meta.featureTypes?.length) {\n <dt>Features</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (feature of sel.entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n </dd>\n }\n </dl>\n\n @if (sel.entry.meta.gqlQuery; as gqlQuery) {\n <h5 class=\"et-query-devtools-section\">GraphQL document</h5>\n <pre class=\"et-query-devtools-code\">{{ gqlDocument(gqlQuery) }}</pre>\n }\n\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Value explorer</h4>\n <input\n #jsonSearchInput\n [value]=\"jsonSearch()\"\n (input)=\"jsonSearch.set(jsonSearchInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter keys / values\u2026\"\n />\n </div>\n\n <h5 class=\"et-query-devtools-section\">Args</h5>\n <et-query-devtools-json\n [value]=\"sel.query.args()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"args\"\n />\n\n @if (sel.query.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Error ({{ error.raw.status }})</h5>\n <et-query-devtools-json\n [value]=\"error.isList ? error.errors : error.error\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"error\"\n />\n } @else {\n <h5 class=\"et-query-devtools-section\">Response</h5>\n <et-query-devtools-json\n [value]=\"sel.query.response()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"response\"\n />\n }\n </ng-template>\n </section>\n}\n\n@if (!open()) {\n <et-query-devtools-toggle (openChange)=\"toggleOpen()\" />\n}\n\n@if (inspectActive() && inspectHover(); as hover) {\n <div\n [style.left.px]=\"hover.rect.left\"\n [style.top.px]=\"hover.rect.top\"\n [style.width.px]=\"hover.rect.width\"\n [style.height.px]=\"hover.rect.height\"\n class=\"et-query-devtools-inspect-box\"\n >\n <span class=\"et-query-devtools-inspect-tag\">{{ inspectLabel(hover.entries) }}</span>\n </div>\n}\n", styles: ["@layer components{.et-query-devtools-host{--_et-qdt-bg: var(--et-surface-background-solid, rgb(24 24 27));--_et-qdt-fg: var(--et-surface-color-solid, rgb(244 244 245));--_et-qdt-muted: var(--et-surface-color-muted-solid, rgb(161 161 170));--_et-qdt-subtle: var(--et-surface-color-subtle-solid, rgb(113 113 122));--_et-qdt-border: var(--et-surface-border-solid, rgb(255 255 255 / .12));--_et-qdt-interaction: var(--et-surface-interaction-solid, rgb(255 255 255));--_et-qdt-accent: var(--et-theme-color-primary-solid, rgb(96 165 250));--_et-qdt-hover: color-mix(in srgb, var(--_et-qdt-interaction) 10%, transparent);--_et-qdt-active: color-mix(in srgb, var(--_et-qdt-interaction) 18%, transparent);--_et-qdt-success: rgb(34 197 94);--_et-qdt-error: rgb(239 68 68);--_et-qdt-loading: rgb(234 179 8);position:fixed;inset-inline:0;inset-block-end:0;z-index:2147483000;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,Liberation Mono,monospace;font-size:1.2rem;line-height:1.4;pointer-events:none}.et-query-devtools-host>*{pointer-events:auto}.et-query-devtools-panel{position:relative;display:flex;flex-direction:column;inline-size:100%;min-block-size:20rem;max-block-size:90vh;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border-block-start:1px solid var(--_et-qdt-border);box-shadow:0 -.4rem 2rem #00000059}.et-query-devtools-panel--resizing{-webkit-user-select:none;user-select:none}.et-query-devtools-resize{position:absolute;inset-inline:0;inset-block-start:0;block-size:.6rem;cursor:ns-resize;z-index:1}.et-query-devtools-resize:after{content:\"\";position:absolute;inset-inline:50%;inset-block-start:.2rem;inline-size:4rem;margin-inline-start:-2rem;block-size:.2rem;border-radius:999px;background:var(--_et-qdt-border)}.et-query-devtools-resize:hover:after{background:var(--_et-qdt-accent)}.et-query-devtools-header{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding-inline:.8rem;border-block-end:1px solid var(--_et-qdt-border);flex:0 0 auto}.et-query-devtools-tabs{display:flex;flex-wrap:wrap;gap:.2rem}.et-query-devtools-tab{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);padding:.9rem 1rem;cursor:pointer;border-block-end:2px solid transparent;font:inherit}.et-query-devtools-tab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-tab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-header-actions{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}.et-query-devtools-inspect-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.4rem .8rem;font:inherit}.et-query-devtools-inspect-btn:hover{color:var(--_et-qdt-fg)}.et-query-devtools-inspect-btn--active{color:#09090b;background:var(--_et-qdt-accent);border-color:var(--_et-qdt-accent)}.et-query-devtools-close{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.6rem;font:inherit}.et-query-devtools-close-panel{appearance:none;display:inline-flex;align-items:center;gap:.6rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:var(--_et-qdt-hover);color:var(--_et-qdt-fg);cursor:pointer;padding:.4rem .6rem .4rem .8rem;font:inherit}.et-query-devtools-close-panel:hover{border-color:var(--_et-qdt-error);color:var(--_et-qdt-error)}.et-query-devtools-kbd{border-radius:.3rem;background:var(--_et-qdt-hover);box-shadow:inset 0 0 0 1px var(--_et-qdt-border);color:var(--_et-qdt-muted);padding:.15rem .4rem;font:inherit;font-size:1.1rem;line-height:1.2;white-space:nowrap}.et-query-devtools-inspect-box{position:fixed;z-index:2147483002;pointer-events:none;border:2px solid var(--_et-qdt-accent);background:color-mix(in srgb,var(--_et-qdt-accent) 15%,transparent);border-radius:.2rem}.et-query-devtools-inspect-tag{position:absolute;inset-block-start:100%;inset-inline-start:0;margin-block-start:.2rem;padding:.2rem .5rem;border-radius:.3rem;background:var(--_et-qdt-accent);color:#09090b;font-family:ui-monospace,monospace;font-size:1.1rem;font-weight:600;white-space:nowrap}.et-query-devtools-close:hover{color:var(--_et-qdt-fg)}.et-query-devtools-body{flex:1 1 auto;min-block-size:0;overflow:hidden}.et-query-devtools-master-detail{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-list{flex:0 0 34%;min-inline-size:22rem;max-inline-size:40rem;overflow-y:auto;border-inline-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail{flex:1 1 auto;min-inline-size:0;overflow-y:auto;padding:1rem}.et-query-devtools-detail .et-query-devtools-detail-head,.et-query-devtools-drawer .et-query-devtools-detail-head{padding-block-end:.8rem;border-block-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail .et-query-devtools-meta,.et-query-devtools-drawer .et-query-devtools-meta{padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-detail .et-query-devtools-section-row,.et-query-devtools-drawer .et-query-devtools-section-row{padding-block-start:.8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-filter-chip{display:inline-flex;align-items:center;gap:.6rem;padding:.3rem .35rem .3rem .7rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-accent) 16%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_et-qdt-accent) 45%,transparent);color:var(--_et-qdt-accent);font-size:1.1rem;font-weight:600;letter-spacing:.02em}.et-query-devtools-filter-icon{font-size:1.3rem;line-height:1}.et-query-devtools-filter-count{border-radius:999px;background:var(--_et-qdt-accent);color:#09090b;min-inline-size:1.8rem;padding:0 .5rem;text-align:center}.et-query-devtools-filter-clear{appearance:none;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.3rem .6rem;font:inherit;font-size:1.15rem;line-height:1.4}.et-query-devtools-filter-clear:hover{border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-scroll{block-size:100%;overflow:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.et-query-devtools-split{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-split .et-query-devtools-scroll{flex:1 1 auto;min-inline-size:0}.et-query-devtools-drawer{flex:0 0 42%;min-inline-size:26rem;max-inline-size:48rem;overflow-y:auto;padding:0 1rem 1rem;border-inline-start:1px solid var(--_et-qdt-border)}.et-query-devtools-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem 0;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg);color:var(--_et-qdt-muted);border-block-end:1px solid var(--_et-qdt-border);margin-block-end:.8rem}.et-query-devtools-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-toolbar--filtered{background:linear-gradient(to right,color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent),transparent 65%),var(--_et-qdt-bg);box-shadow:inset .2rem 0 0 var(--_et-qdt-accent),inset 0 -1px color-mix(in srgb,var(--_et-qdt-accent) 30%,transparent)}.et-query-devtools-field{display:inline-flex;align-items:center;gap:.6rem;color:var(--_et-qdt-muted)}.et-query-devtools-select{background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-count{color:var(--_et-qdt-subtle)}.et-query-devtools-row{display:flex;align-items:center;gap:.8rem;inline-size:100%;text-align:start;appearance:none;border:none;border-block-end:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-fg);padding:.8rem;cursor:pointer;font:inherit}.et-query-devtools-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-row--active{background:var(--_et-qdt-active)}.et-query-devtools-method{font-weight:700;font-size:1.1rem;color:var(--_et-qdt-muted);white-space:nowrap}.et-query-devtools-route{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-inline-size:0}.et-query-devtools-status{inline-size:.8rem;block-size:.8rem;border-radius:999px;flex:0 0 auto;background:var(--_et-qdt-subtle)}.et-query-devtools-status[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-status[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-status[data-status=loading],.et-query-devtools-status[data-status=running]{background:var(--_et-qdt-loading)}.et-query-devtools-detail-head{display:flex;align-items:center;gap:.8rem;margin-block-end:.8rem}.et-query-devtools-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-block-end:.8rem}.et-query-devtools-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.3rem;background:transparent;color:var(--_et-qdt-fg);padding:.25rem .55rem;font-size:1.15rem;line-height:1.4;cursor:pointer}.et-query-devtools-btn:hover{background:var(--_et-qdt-hover)}.et-query-devtools-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;margin:0 0 1rem}.et-query-devtools-meta dt{color:var(--_et-qdt-muted)}.et-query-devtools-meta dd{margin:0;overflow-wrap:anywhere}.et-query-devtools-section{margin:1rem 0 .4rem;font-size:1.1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-json{margin:0;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere;max-block-size:24rem;overflow-y:auto}.et-query-devtools-json--error{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-mono{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-code{margin:0 0 .8rem;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1.15rem;max-block-size:20rem}.et-query-devtools-ws-message{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;margin-block-end:.4rem}.et-query-devtools-url{display:inline-block;max-inline-size:32rem;margin-inline-start:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.et-query-devtools-chips,.et-query-devtools-chip-row{display:flex;flex-wrap:wrap;gap:.4rem;margin-block-end:.8rem}.et-query-devtools-chip{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .6rem;border-radius:999px;border:1px solid var(--_et-qdt-border);font-size:1.1rem;color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-interaction) 6%,transparent)}.et-query-devtools-chip--muted{color:var(--_et-qdt-muted)}.et-query-devtools-chip--loading{color:var(--_et-qdt-loading);border-color:color-mix(in srgb,var(--_et-qdt-loading) 50%,transparent)}.et-query-devtools-chip--error{color:var(--_et-qdt-error);border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-chip--success{color:var(--_et-qdt-success);border-color:color-mix(in srgb,var(--_et-qdt-success) 50%,transparent)}.et-query-devtools-card{border:1px solid var(--_et-qdt-border);border-radius:.6rem;padding:1rem}.et-query-devtools-card-head{display:flex;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-card-title{display:flex;align-items:baseline;gap:.5rem;margin-block-end:.2rem}.et-query-devtools-card-sub{color:var(--_et-qdt-subtle);font-size:1.1rem;margin-block-end:.6rem;overflow-wrap:anywhere}.et-query-devtools-inner-queries{display:flex;flex-direction:column;gap:.3rem}.et-query-devtools-inner-row{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-fg);padding:.4rem .6rem;cursor:pointer;font:inherit}.et-query-devtools-inner-row:hover:not([disabled]){background:var(--_et-qdt-hover)}.et-query-devtools-inner-row--active{background:var(--_et-qdt-active);border-color:var(--_et-qdt-accent)}.et-query-devtools-inner-row[disabled]{cursor:default;opacity:.6}.et-query-devtools-step-list{display:flex;flex-direction:column;gap:.4rem;margin:0;padding:0;list-style:none}.et-query-devtools-step-item{border:1px solid var(--_et-qdt-border);border-radius:.4rem;overflow:hidden}.et-query-devtools-step-item[data-status=error]{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-step-head{display:flex;align-items:stretch}.et-query-devtools-step-head .et-query-devtools-inner-row{border:none;border-radius:0;flex:1 1 auto}.et-query-devtools-step-caret-btn{appearance:none;border:none;border-inline-start:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:0 .7rem;font:inherit}.et-query-devtools-step-caret-btn:hover{background:var(--_et-qdt-hover);color:var(--_et-qdt-fg)}.et-query-devtools-step-toggle{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:none;background:transparent;color:var(--_et-qdt-fg);padding:.6rem;cursor:pointer;font:inherit}.et-query-devtools-step-toggle:hover{background:var(--_et-qdt-hover)}.et-query-devtools-step-index{flex:1 1 auto;min-inline-size:0}.et-query-devtools-step-caret{color:var(--_et-qdt-muted)}.et-query-devtools-step-io{padding:0 .8rem .8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-table{inline-size:100%;border-collapse:collapse}.et-query-devtools-table th,.et-query-devtools-table td{text-align:start;padding:.5rem .6rem;border-block-end:1px solid var(--_et-qdt-border);vertical-align:top}.et-query-devtools-table th{color:var(--_et-qdt-muted);font-weight:600;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-table tr[data-status=error] td{color:var(--_et-qdt-error)}.et-query-devtools-empty{color:var(--_et-qdt-subtle);padding:1.2rem;text-align:center}.et-query-devtools-actions-label{color:var(--_et-qdt-subtle);align-self:center;text-transform:uppercase;font-size:1rem;letter-spacing:.05em}.et-query-devtools-actions-label:not(:first-child){margin-inline-start:.7rem}.et-query-devtools-cell-actions{display:flex;gap:.4rem;white-space:nowrap}.et-query-devtools-section-row{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-block-start:1rem}.et-query-devtools-search{flex:0 1 20rem;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-editor{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.8rem;margin-block-end:1rem}.et-query-devtools-textarea{inline-size:100%;min-block-size:12rem;resize:vertical;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;font-family:inherit;font-size:1.2rem}.et-query-devtools-editor-error{color:var(--_et-qdt-error);margin:.4rem 0 0}.et-query-devtools-json-node,.et-query-devtools-json-children{display:flex;flex-direction:column}.et-query-devtools-json-children{padding-inline-start:1.4rem;border-inline-start:1px solid var(--_et-qdt-border);margin-inline-start:.4rem}.et-query-devtools-json-row{display:flex;align-items:baseline;gap:.4rem;padding:.1rem 0;font-family:inherit;line-height:1.5}.et-query-devtools-json-toggle{appearance:none;display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;border:none;background:transparent;color:var(--_et-qdt-fg);cursor:pointer;text-align:start;font:inherit;padding:.1rem 0}.et-query-devtools-json-node>.et-query-devtools-json-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-json-caret{color:var(--_et-qdt-subtle);inline-size:1rem;flex:0 0 auto}.et-query-devtools-json-key{color:var(--_et-qdt-accent)}.et-query-devtools-json-colon,.et-query-devtools-json-preview{color:var(--_et-qdt-subtle)}.et-query-devtools-json-value{overflow-wrap:anywhere}.et-query-devtools-json-leaf[data-kind=string] .et-query-devtools-json-value{color:var(--_et-qdt-success)}.et-query-devtools-json-leaf[data-kind=number] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=boolean] .et-query-devtools-json-value{color:var(--_et-qdt-loading)}.et-query-devtools-json-leaf[data-kind=null] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=undefined] .et-query-devtools-json-value{color:var(--_et-qdt-subtle)}.et-query-devtools-json-hit{background:color-mix(in srgb,var(--_et-qdt-loading) 40%,transparent);border-radius:.2rem}.et-query-devtools-json-copy{appearance:none;flex:0 0 auto;border:none;background:transparent;color:var(--_et-qdt-subtle);cursor:pointer;opacity:0;font:inherit}.et-query-devtools-json-copy:hover:not(.et-query-devtools-json-copy--copied){color:var(--_et-qdt-fg)}.et-query-devtools-json-row:hover .et-query-devtools-json-copy,.et-query-devtools-json-copy:focus-visible,.et-query-devtools-json-copy--copied{opacity:1}.et-query-devtools-json-copy--copied{color:var(--_et-qdt-success)}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QueryDevtoolsJsonComponent, selector: "et-query-devtools-json", inputs: ["value", "nodeKey", "depth", "search", "path", "expandedPaths", "collapsedPaths", "toggleFn"] }, { kind: "component", type: QueryDevtoolsToggleComponent, selector: "et-query-devtools-toggle", outputs: ["openChange"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
49330
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: QueryDevtoolsComponent, isStandalone: true, selector: "et-query-devtools", host: { classAttribute: "et-query-devtools-host" }, ngImport: i0, template: "@if (open()) {\n <section\n [class.et-query-devtools-panel--resizing]=\"resizing()\"\n [style.height.px]=\"panelHeight()\"\n class=\"et-query-devtools-panel\"\n >\n <div\n (pointerdown)=\"startResize($event)\"\n class=\"et-query-devtools-resize\"\n title=\"Drag to resize\"\n aria-hidden=\"true\"\n ></div>\n <header class=\"et-query-devtools-header\">\n <nav class=\"et-query-devtools-tabs\" role=\"tablist\">\n @for (tab of tabs; track tab.id) {\n @let badge = tabBadges()[tab.id];\n <button\n [class.et-query-devtools-tab--active]=\"activeTab() === tab.id\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n (click)=\"activeTab.set(tab.id)\"\n class=\"et-query-devtools-tab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n @if (badge.count) {\n <span class=\"et-query-devtools-tab-badge\">{{ badge.count }}</span>\n }\n @if (badge.errors) {\n <span\n class=\"et-query-devtools-tab-badge et-query-devtools-tab-badge--error\"\n title=\"{{ badge.errors }} {{ badge.errorNoun ?? 'failing' }}\"\n >\n {{ badge.errors }}\n </span>\n }\n </button>\n }\n </nav>\n\n <div class=\"et-query-devtools-header-actions\">\n <button\n [class.et-query-devtools-inspect-btn--active]=\"inspectActive()\"\n [attr.aria-pressed]=\"inspectActive()\"\n (click)=\"toggleInspect()\"\n class=\"et-query-devtools-inspect-btn\"\n type=\"button\"\n title=\"Inspect: hover the UI to find its query\"\n >\n <svg\n class=\"et-query-devtools-inspect-icon\"\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle class=\"et-query-devtools-inspect-icon-ring\" cx=\"8\" cy=\"8\" r=\"4.25\" />\n <circle class=\"et-query-devtools-inspect-icon-dot\" cx=\"8\" cy=\"8\" r=\"1.15\" />\n <path d=\"M8 0.9v2.1M8 13v2.1M0.9 8h2.1M13 8h2.1\" />\n </svg>\n {{ inspectActive() ? 'Inspecting' : 'Inspect' }}\n @if (inspectActive()) {\n <kbd class=\"et-query-devtools-kbd et-query-devtools-kbd--inverse\">Esc</kbd>\n }\n </button>\n <button\n (click)=\"toggleOpen()\"\n class=\"et-query-devtools-close-panel\"\n type=\"button\"\n title=\"Close devtools ({{ shortcut }})\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Close\n <kbd class=\"et-query-devtools-kbd\">{{ shortcut }}</kbd>\n </button>\n </div>\n </header>\n\n @if (armedFaultClients(); as armed) {\n <div class=\"et-query-devtools-armed-bar\" role=\"status\">\n <span class=\"et-query-devtools-armed-dot\" aria-hidden=\"true\"></span>\n <span class=\"et-query-devtools-armed-text\">\n <strong>Faults armed</strong> on\n <span class=\"et-query-devtools-mono\">{{ armed.join(', ') }}</span>\n \u2014 every failure and delay you see from\n {{ armed.length === 1 ? 'this client' : 'these clients' }} is injected, not the API.\n </span>\n <button\n [disabled]=\"activeTab() === 'faults'\"\n (click)=\"activeTab.set('faults')\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Review\n </button>\n <button (click)=\"CLEAR_FAULTS()\" class=\"et-query-devtools-btn\" type=\"button\">Disarm all</button>\n </div>\n }\n\n <div class=\"et-query-devtools-body\">\n @switch (activeTab()) {\n <!-- QUERIES -->\n @case ('queries') {\n <div class=\"et-query-devtools-master-detail\">\n <div class=\"et-query-devtools-list\">\n <div [class.et-query-devtools-toolbar--filtered]=\"!!inspectFilterIds()\" class=\"et-query-devtools-toolbar\">\n <div class=\"et-query-devtools-toolbar-row\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n <button\n (click)=\"clearInspectFilter()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the inspection filter and show all queries\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Clear\n </button>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #clientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(clientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n }\n <span class=\"et-query-devtools-count\">\n @if (isQueryListNarrowed()) {\n {{ filteredQueries().length }} of {{ scopedQueryCount() }}\n } @else {\n {{ scopedQueryCount() }}\n }\n </span>\n <button\n [disabled]=\"!filteredQueries().length\"\n (click)=\"downloadInsomniaCollection()\"\n class=\"et-query-devtools-btn et-query-devtools-toolbar-btn\"\n type=\"button\"\n title=\"Download the listed queries as an Insomnia collection, with the auth provider's token refresh\"\n >\n \u2913 Insomnia\n </button>\n </div>\n\n <div class=\"et-query-devtools-toolbar-row\">\n <input\n #queryFilterInput\n [value]=\"queryFilter()\"\n (input)=\"queryFilter.set(queryFilterInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter method / route / url\u2026\"\n aria-label=\"Filter queries\"\n />\n @for (facet of facets; track facet.id) {\n @let count = facetCounts()[facet.id];\n @let isActive = queryFacets().has(facet.id);\n <button\n [class.et-query-devtools-facet--active]=\"isActive\"\n [attr.aria-pressed]=\"isActive\"\n [disabled]=\"!count && !isActive\"\n (click)=\"toggleFacet(facet.id)\"\n class=\"et-query-devtools-chip et-query-devtools-facet\"\n type=\"button\"\n >\n {{ facet.label }}\n <span class=\"et-query-devtools-facet-count\">{{ count }}</span>\n </button>\n }\n @if (isQueryListNarrowed()) {\n <button\n (click)=\"clearQueryFilters()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the search term and the status chips\"\n aria-label=\"Clear filters\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n </button>\n }\n </div>\n </div>\n\n @for (item of filteredQueries(); track item.entry.id) {\n <button\n [class.et-query-devtools-row--active]=\"selectedQueryId() === item.entry.id\"\n (click)=\"selectedQueryId.set(item.entry.id)\"\n class=\"et-query-devtools-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"queryStatus(item.query)\"\n [title]=\"queryStatus(item.query)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ item.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: routeSegments(item.entry, item.query), fallback: '\u2014' }\n \"\n />\n </span>\n <!-- A retry is the one thing a loading dot actively misrepresents: it reads as a request\n on its way when nothing is in flight at all. -->\n @if (requestProgress(item.query)?.retry; as retry) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Waiting out a retry backoff before attempt {{ retry.attempt }}\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ retry.attempt }}\n </span>\n }\n @if (isStale(item.query)) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">stale</span>\n }\n </button>\n } @empty {\n <!-- An empty scope is a different problem from a filter that matches nothing, and only the\n latter is the user's own doing - so it names the way out. -->\n @if (scopedQueryCount()) {\n <p class=\"et-query-devtools-empty\">\n No query matches the filter \u00B7 {{ scopedQueryCount() }} in scope.\n </p>\n } @else if (inspectFilterIds()) {\n <p class=\"et-query-devtools-empty\">\n The inspected element's queries are no longer registered. Clear the filter.\n </p>\n } @else {\n <p class=\"et-query-devtools-empty\">No queries registered.</p>\n }\n }\n </div>\n\n <div class=\"et-query-devtools-detail\">\n @if (selectedQuery(); as sel) {\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Select a query to inspect it.</p>\n }\n </div>\n </div>\n }\n\n <!-- STACKS -->\n @case ('stacks') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of stackEntries(); track entry.id) {\n @if (entry.kind === 'paged-query-stack') {\n @let paged = asPagedStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">paged stack</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ paged.direction() }}</span>\n @if (paged.loading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n </div>\n\n @let pagedId = stackIdentity(paged);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ pagedId.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: pagedId.segments, fallback: '\u2014' }\"\n />\n </span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ pagedId.baseUrl }}</div>\n @if (entry.meta.features?.length) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Pages loaded</dt>\n <dd>{{ paged.queries().length }}</dd>\n <dt>Items</dt>\n <dd>{{ paged.items().length }}</dd>\n <dt>First page loaded</dt>\n <dd>{{ paged.isFirstPageLoaded() }}</dd>\n <dt>Last page loaded</dt>\n <dd>{{ paged.isLastPageLoaded() }}</dd>\n <ng-container *ngTemplateOutlet=\"activityRowTpl; context: { activity: stackActivity(paged) }\" />\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(paged) }\" />\n </div>\n } @else {\n @let stack = asStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">stack</span>\n @if (stack.anyLoading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n @if (stack.anyError()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">error</span>\n }\n </div>\n\n @let stackId = stackIdentity(stack);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ stackId.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: stackId.segments, fallback: '\u2014' }\"\n />\n </span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ stackId.baseUrl }}</div>\n @if (entry.meta.features?.length) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Queries</dt>\n <dd>{{ stack.queries().length }}</dd>\n <dt>Loading</dt>\n <dd>{{ stack.loadingProgress().loaded }} / {{ stack.loadingProgress().total }}</dd>\n <dt>Errors</dt>\n <dd>{{ stack.errors().length }}</dd>\n <ng-container *ngTemplateOutlet=\"activityRowTpl; context: { activity: stackActivity(stack) }\" />\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(stack) }\" />\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No stacks registered.</p>\n }\n </div>\n\n @if (stackSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"stackSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- SEQUENCES -->\n @case ('sequences') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of sequenceEntries(); track entry.id) {\n @let sequence = asSequence(entry);\n @if (sequence) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">sequence</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ sequence.status() }}</span>\n <span class=\"et-query-devtools-count\"\n >step {{ sequence.currentStep() }} / {{ sequence.total }}</span\n >\n </div>\n\n <dl class=\"et-query-devtools-meta\">\n <ng-container\n *ngTemplateOutlet=\"activityRowTpl; context: { activity: sequenceActivity(sequence) }\"\n />\n </dl>\n\n <ol class=\"et-query-devtools-step-list\">\n @for (link of queriesForSequence(sequence); track $index; let i = $index) {\n <li [attr.data-status]=\"sequenceStepStatus(sequence, i)\" class=\"et-query-devtools-step-item\">\n <div class=\"et-query-devtools-step-head\">\n <button\n [class.et-query-devtools-inner-row--active]=\"sequenceSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"sequenceSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"sequenceStepStatus(sequence, i)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ i + 1 }}. {{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: link.segments, fallback: 'step ' + (i + 1) }\n \"\n />\n </span>\n </button>\n <button\n [attr.aria-expanded]=\"isStepExpanded(entry.id, i)\"\n (click)=\"toggleStep(entry.id, i)\"\n class=\"et-query-devtools-step-caret-btn\"\n type=\"button\"\n title=\"Toggle in / out\"\n >\n {{ isStepExpanded(entry.id, i) ? '\u25BE' : '\u25B8' }}\n </button>\n </div>\n\n @if (isStepExpanded(entry.id, i)) {\n @let snap = stepSnapshot(sequence, i);\n <div class=\"et-query-devtools-step-io\">\n <h5 class=\"et-query-devtools-section\">In \u00B7 args</h5>\n @if (sequence.stepArgs().length > i) {\n <et-query-devtools-json [value]=\"sequence.stepArgs()[i]\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n\n @if (snap?.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 error ({{ error.raw.status }})</h5>\n <et-query-devtools-json [value]=\"error.isList ? error.errors : error.error\" />\n } @else {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 response</h5>\n @if (snap) {\n <et-query-devtools-json [value]=\"snap.response()\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n }\n </div>\n }\n </li>\n }\n </ol>\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No sequences registered.</p>\n }\n </div>\n\n @if (sequenceSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"sequenceSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- FORMS -->\n @case ('forms') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of formEntries(); track entry.id) {\n @let form = asForm(entry);\n @let expanded = selectedFormId() === entry.id;\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <button\n [attr.aria-expanded]=\"expanded\"\n (click)=\"selectedFormId.set(expanded ? null : entry.id)\"\n class=\"et-query-devtools-row-link\"\n type=\"button\"\n >\n <span class=\"et-query-devtools-caret\">{{ expanded ? '\u25BE' : '\u25B8' }}</span>\n <strong>{{ entry.meta.name || 'query form' }}</strong>\n </button>\n @if (form.activeFilterCount(); as count) {\n <span class=\"et-query-devtools-chip\">{{ count }} active</span>\n } @else {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">at defaults</span>\n }\n @if (form.isCommitPending()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">debouncing</span>\n }\n @if (!form.isObserving()) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"observe() has not been called, so nothing syncs with the URL\"\n >\n not observing\n </span>\n }\n <button\n [disabled]=\"form.isAtDefaults()\"\n (click)=\"form.resetAll()\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n \u27F2 Reset all\n </button>\n </div>\n\n @let driven = queriesDrivenByForm(entry);\n <dl class=\"et-query-devtools-meta\">\n <dt>Drives</dt>\n <dd>\n @if (driven.length) {\n <ng-container *ngTemplateOutlet=\"formDrivesTpl; context: { links: driven }\" />\n } @else {\n <span class=\"et-query-devtools-hint\">\n No query has read this form's value yet. A query picks it up the first time its\n <code>withArgs</code> reads <code>value()</code>.\n </span>\n }\n </dd>\n </dl>\n\n @if (expanded) {\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Field</th>\n <th>Value</th>\n <th>Default</th>\n <th>URL param</th>\n <th>Rules</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (field of form.fields(); track field.key) {\n <tr [attr.data-status]=\"field.isDefault ? null : 'success'\">\n <td class=\"et-query-devtools-mono\">{{ field.key }}</td>\n <td class=\"et-query-devtools-mono\">\n {{ inlineValue(field.value) }}\n <!-- The live value only differs while a debounce is pending, which is exactly\n when \"the input says X but the query asked for Y\" needs explaining. -->\n @if (field.liveValue !== field.value) {\n <span class=\"et-query-devtools-stat-sub\"\n >typing {{ inlineValue(field.liveValue) }}</span\n >\n }\n </td>\n <td class=\"et-query-devtools-mono\">{{ inlineValue(field.defaultValue) }}</td>\n <td class=\"et-query-devtools-mono\">\n @if (field.queryParam === undefined) {\n <span class=\"et-query-devtools-hint\">\u2014</span>\n } @else {\n {{ field.paramKey }}={{ inlineValue(field.queryParam) }}\n }\n </td>\n <td>\n <div class=\"et-query-devtools-chips\">\n @if (field.debounceMs !== null) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">\n {{ field.debounceMs }}ms\n </span>\n }\n @if (field.isResetBy.length) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Reset to its default whenever one of these changes\"\n >\n reset by {{ field.isResetBy.join(', ') }}\n </span>\n }\n @if (!field.countsAsFilter) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Not counted by activeFilterCount\"\n >\n not a filter\n </span>\n }\n </div>\n </td>\n <td>\n <button\n [disabled]=\"field.isDefault\"\n (click)=\"form.resetField(field.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n \u27F2\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n\n <h4 class=\"et-query-devtools-section\">Committed value</h4>\n <et-query-devtools-json [value]=\"form.value()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">\n No query forms registered. A form created with <code>createQueryForm()</code> registers itself.\n </p>\n }\n </div>\n\n @if (formSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"formSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- AUTH -->\n @case ('auth') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of authEntries(); track entry.id) {\n @let auth = asAuth(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'auth provider' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"auth.isAuthenticated()\"\n [class.et-query-devtools-chip--muted]=\"!auth.isAuthenticated()\"\n class=\"et-query-devtools-chip\"\n >\n {{ auth.isAuthenticated() ? 'authenticated' : 'anonymous' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.clientBaseUrl || entry.meta.clientName || '\u2014' }}</dd>\n <dt>Access token</dt>\n <dd>{{ auth.accessToken() ? 'present' : 'none' }}</dd>\n <dt>Refresh token</dt>\n <dd>{{ auth.refreshToken() ? 'present' : 'none' }}</dd>\n @if (authTokenExpiry(auth); as expiry) {\n <dt>Token expires in</dt>\n <dd>{{ expiry }}</dd>\n }\n @if (auth.executionState(); as state) {\n <dt>Execution state</dt>\n <dd>{{ state.type }} \u00B7 {{ state.state }}</dd>\n }\n @if (authQueryKeys(auth).length) {\n <dt>Queries</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (key of authQueryKeys(auth); track key) {\n <span class=\"et-query-devtools-chip\">{{ key }}</span>\n }\n </div>\n </dd>\n }\n @if (entry.meta.features?.length) {\n <dt>Features</dt>\n <dd>\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n </dd>\n }\n </dl>\n\n @if (authTokenPayload(auth); as payload) {\n <h4 class=\"et-query-devtools-section\">Access token payload</h4>\n <et-query-devtools-json [value]=\"payload\" />\n }\n\n @if (auth.latestNonInternalQuery(); as latest) {\n <h4 class=\"et-query-devtools-section\">Latest auth query \u00B7 {{ latest.key }}</h4>\n <et-query-devtools-json [value]=\"latest.snapshot.response()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No auth providers registered.</p>\n }\n </div>\n }\n\n <!-- SOCKETS -->\n @case ('ws') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of wsEntries(); track entry.id) {\n @let ws = asWs(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'socket' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"ws.connected()\"\n [class.et-query-devtools-chip--muted]=\"!ws.connected()\"\n class=\"et-query-devtools-chip\"\n >\n {{ ws.connected() ? 'connected' : 'disconnected' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.url }}</dd>\n <dt>Rooms</dt>\n <dd>\n @if (ws.rooms().length) {\n <div class=\"et-query-devtools-chips\">\n @for (room of ws.rooms(); track room) {\n <span class=\"et-query-devtools-chip\">{{ room }}</span>\n }\n </div>\n } @else {\n none\n }\n </dd>\n </dl>\n\n <h5 class=\"et-query-devtools-section\">Messages ({{ ws.messages().length }})</h5>\n @for (message of ws.messages(); track message.id) {\n <div class=\"et-query-devtools-ws-message\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-count\">{{ formatTime(message.timestamp) }}</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ message.room }}</span>\n <span class=\"et-query-devtools-method\">{{ message.event }}</span>\n </div>\n <et-query-devtools-json [value]=\"message.data\" />\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No messages received yet.</p>\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No web socket clients registered.</p>\n }\n </div>\n }\n\n <!-- CACHE -->\n @case ('cache') {\n <div class=\"et-query-devtools-scroll\">\n @for (client of cacheView(); track client.name) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.baseUrl || client.name }}</strong>\n <span class=\"et-query-devtools-count\">{{ client.entries.length }} entries</span>\n @if (client.client?.subtle?.persistence) {\n <span class=\"et-query-devtools-count\">{{ persistedCount(client.client!) }} on disk</span>\n <button (click)=\"clearPersistedQueries(client.client!)\" class=\"et-query-devtools-btn\" type=\"button\">\n Clear disk\n </button>\n }\n </div>\n @if (clientFeatures(client.client); as features) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features }\" />\n }\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Request</th>\n <th>Consumers</th>\n <th>Secure</th>\n <th>Freshness</th>\n <th>Sync</th>\n <th>Disk</th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (cacheEntry of client.entries; track cacheEntry.key) {\n <tr>\n <td [title]=\"cacheEntry.request.method + ' ' + cacheEntry.request.url\">\n <span class=\"et-query-devtools-method\">{{ cacheEntry.request.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(cacheEntry.request.url) }}</span>\n </td>\n <td>{{ cacheEntry.consumerCount }}</td>\n <td>{{ cacheEntry.isSecure ? 'yes' : 'no' }}</td>\n <td>{{ cacheFreshness(cacheEntry) }}</td>\n <td>{{ cacheSync(cacheEntry, client.pollStates) }}</td>\n <td>{{ cachePersistence(cacheEntry) }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n <button (click)=\"refetchCacheEntry(cacheEntry)\" class=\"et-query-devtools-btn\" type=\"button\">\n Refetch\n </button>\n <button\n (click)=\"evictCacheEntry(client.repository, cacheEntry.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Evict\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"7\">Cache empty.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No clients with a cache yet.</p>\n }\n </div>\n }\n\n <!-- TIMELINE -->\n @case ('timeline') {\n @let tl = timeline();\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-pane\">\n <div class=\"et-query-devtools-toolbar\">\n <div class=\"et-query-devtools-toolbar-row\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #timelineClientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(timelineClientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n }\n <span class=\"et-query-devtools-count\">\n {{ tl.rows.length }} run{{ tl.rows.length === 1 ? '' : 's' }}\n @if (tl.hidden) {\n \u00B7 {{ tl.hidden }} older not shown\n }\n @if (tl.rows.length) {\n \u00B7 over {{ formatDuration(tl.windowMs) }} from {{ formatTime(tl.startedAt) }}\n }\n </span>\n <button\n [disabled]=\"!tl.rows.length\"\n (click)=\"resetTimeline()\"\n class=\"et-query-devtools-btn et-query-devtools-toolbar-btn\"\n type=\"button\"\n title=\"Clear the run history and the counters of every listed query\"\n >\n \u27F2 Reset\n </button>\n </div>\n </div>\n\n @if (tl.rows.length) {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-timeline\">\n <div class=\"et-query-devtools-timeline-axis\">\n <span class=\"et-query-devtools-timeline-ticks\">\n @for (tick of timelineTicks(); track tick.pct) {\n <span [style.left.%]=\"tick.pct\" class=\"et-query-devtools-timeline-tick\">{{\n tick.label\n }}</span>\n }\n </span>\n </div>\n\n @for (row of tl.rows; track row.key) {\n <button\n [class.et-query-devtools-timeline-row--active]=\"timelineSelectedQueryId() === row.entryId\"\n (click)=\"selectTimelineRow(row)\"\n class=\"et-query-devtools-timeline-row\"\n type=\"button\"\n title=\"{{ row.method }} {{ row.path }} \u00B7 run {{ row.run.index }} \u00B7 {{ row.run.status }}\"\n >\n <span class=\"et-query-devtools-timeline-label\">\n <span class=\"et-query-devtools-method\">{{ row.method }}</span>\n <span class=\"et-query-devtools-url\">{{ row.path }}</span>\n </span>\n <span class=\"et-query-devtools-timeline-track\">\n <span\n [attr.data-status]=\"runStatus(row.run)\"\n [style.left.%]=\"row.leftPct\"\n [style.width.%]=\"row.widthPct\"\n class=\"et-query-devtools-timeline-bar\"\n ></span>\n </span>\n <span class=\"et-query-devtools-timeline-duration\">\n @if (row.durationMs === null) {\n in flight\n } @else {\n {{ formatDuration(row.durationMs) }}\n }\n </span>\n <span class=\"et-query-devtools-timeline-size\">\n @if (row.run.receivedBytes) {\n {{ formatBytes(row.run.receivedBytes) }}\n }\n </span>\n <span class=\"et-query-devtools-timeline-flags\">\n @if (row.run.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Took {{\n row.run.attempts\n }} attempts - so this bar is mostly retry backoff, not one slow round trip\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ row.run.attempts }}\n </span>\n }\n @if (!row.run.didRequest) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Arrived without a request of this query's own - a poll, another consumer of the same cache entry, or another tab\"\n >\n shared\n </span>\n }\n @if (row.run.status === 'aborted') {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"The query requested again before this run's response arrived\"\n >\n aborted\n </span>\n }\n </span>\n </button>\n }\n </div>\n </div>\n } @else {\n <p class=\"et-query-devtools-empty\">\n No requests recorded yet. Every request a listed query makes shows up here as a bar.\n </p>\n }\n </div>\n\n @if (timelineSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"timelineSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- EVENTS -->\n @case ('events') {\n <div class=\"et-query-devtools-pane\">\n <div class=\"et-query-devtools-toolbar\">\n <span class=\"et-query-devtools-count\">{{ eventLog().length }} events</span>\n <button (click)=\"clearEvents()\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n <div class=\"et-query-devtools-scroll\">\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Base URL</th>\n <th>Type</th>\n <th>Request</th>\n </tr>\n </thead>\n <tbody>\n @for (event of eventLog(); track event.id) {\n <tr\n [attr.data-status]=\"\n event.type === 'request-error' ? 'error' : event.type === 'queries-refreshed' ? null : 'success'\n \"\n >\n <td>{{ formatTime(event.timestamp) }}</td>\n <td class=\"et-query-devtools-mono\">{{ event.client }}</td>\n <td>\n {{ eventTypeLabel(event) }}\n @if (event.isSecure) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">secure</span>\n }\n </td>\n @if (event.cause; as cause) {\n <td>\n <span class=\"et-query-devtools-url\">{{ causeLabel(cause) }}</span>\n @if (event.refreshed?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (refreshed of event.refreshed; track $index) {\n @if (refreshed.queryIds[0]; as queryId) {\n <button\n (click)=\"selectQuery(queryId)\"\n class=\"et-query-devtools-chip et-query-devtools-chip--button\"\n type=\"button\"\n title=\"Open {{ refreshed.method }} {{ refreshed.path }}\"\n >\n {{ refreshed.method }} {{ refreshed.path }}\n <!-- One request, several queries on the same cache key. Spelling the count\n out beats repeating an identical row per consumer. -->\n @if (refreshed.queryIds.length > 1) {\n <small>\u00D7{{ refreshed.queryIds.length }}</small>\n }\n </button>\n } @else {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"No registered query holds this cache entry any more\"\n >\n {{ refreshed.method }} {{ refreshed.path }}\n </span>\n }\n }\n </div>\n } @else {\n <span class=\"et-query-devtools-hint\">matched no query in use</span>\n }\n </td>\n } @else if (event.url; as url) {\n <td>\n @if (event.queryId) {\n <button\n (click)=\"selectEventRow(event)\"\n class=\"et-query-devtools-row-link\"\n type=\"button\"\n title=\"Open {{ event.method }} {{ url }}\"\n >\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </button>\n } @else {\n <span [title]=\"event.method + ' ' + url\">\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </span>\n }\n </td>\n } @else {\n <td class=\"et-query-devtools-url\">all secure entries dropped</td>\n }\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"4\">No events captured yet.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n }\n\n <!-- FAULTS -->\n @case ('faults') {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-toolbar\">\n <p class=\"et-query-devtools-hint\">\n Faults are injected into the request pipeline, so retries, error handling features and the cache see\n them exactly as they would see a real one. Unlike the rest of the panel, what you arm here is not\n persisted - a page reload disarms every client.\n </p>\n </div>\n\n @for (client of faultClients(); track client.name) {\n @let fault = client.fault;\n <div [class.et-query-devtools-card--armed]=\"client.armed\" class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.name }}</strong>\n <span class=\"et-query-devtools-url\">{{ client.baseUrl }}</span>\n @if (client.armed) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">armed</span>\n <button (click)=\"CLEAR_FAULTS(client.name)\" class=\"et-query-devtools-btn\" type=\"button\">\n Disarm\n </button>\n }\n </div>\n\n <div class=\"et-query-devtools-faults\">\n <label class=\"et-query-devtools-field\">\n <span>Latency</span>\n <input\n #latencyInput\n [value]=\"fault.latencyMs\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'latencyMs', value: latencyInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"60000\"\n step=\"100\"\n />\n <small>ms before every attempt starts</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Fail next</span>\n <input\n #failNextInput\n [value]=\"fault.failNext\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'failNext', value: failNextInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"99\"\n step=\"1\"\n />\n <small>attempts, counted down as they are spent</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Fail rate</span>\n <input\n #failRateInput\n [value]=\"fault.failRate\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'failRate', value: failRateInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"5\"\n />\n <small>% of attempts, rolled per attempt</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Status</span>\n <select\n #statusSelect\n (change)=\"armFaultStatus({ clientName: client.name, value: statusSelect.value })\"\n class=\"et-query-devtools-select\"\n >\n @for (option of FAULT_STATUSES; track option.status) {\n <!-- `selected` on the option, not `value` on the select: the select's value is\n applied before its options exist, so it would fall back to the first one. -->\n <option [value]=\"option.status\" [selected]=\"option.status === fault.status\">\n {{ option.status }} \u00B7 {{ option.label }}\n </option>\n }\n </select>\n <small>\n @if (isFaultStatusRetryable(fault.status)) {\n the default retry policy retries this\n } @else {\n the default retry policy gives up on this\n }\n </small>\n </label>\n </div>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No query clients registered yet.</p>\n }\n </div>\n }\n }\n </div>\n\n <!-- A route with its path params and query string picked out from the static path. The segments must\n stay element-only: a text node between them would render as a space inside the route. -->\n <ng-template #routeTpl let-segments=\"segments\" let-fallback=\"fallback\">\n @for (segment of segments; track $index) {\n <span [class]=\"'et-query-devtools-route-' + segment.kind\" [attr.title]=\"segment.name\">{{ segment.text }}</span>\n } @empty {\n {{ fallback }}\n }\n </ng-template>\n\n <!-- Reusable dt/dd pair for a card's `et-query-devtools-meta` list: what its queries ran and moved. -->\n <ng-template #activityRowTpl let-activity=\"activity\">\n <dt>Traffic</dt>\n <dd>\n @if (activity.hasActivity) {\n {{ activity.stats.requests }} request{{ activity.stats.requests === 1 ? '' : 's' }} \u00B7 \u2193\n {{ formatTransferred(activity.stats.receivedBytes, activity.stats.hasEstimatedBytes) }}\n @if (activity.stats.sentBytes) {\n \u00B7 \u2191 {{ formatTransferred(activity.stats.sentBytes, activity.stats.hasEstimatedBytes) }}\n }\n @if (activity.cacheServed) {\n \u00B7 {{ activity.cacheServed }} from cache\n }\n @if (activity.stats.errors) {\n \u00B7 {{ activity.stats.errors }} failed\n }\n } @else {\n \u2014\n }\n </dd>\n </ng-template>\n\n <!-- Reusable feature list: the feature's name, followed by the options it was configured with. -->\n <ng-template #featuresTpl let-features=\"features\">\n <ul class=\"et-query-devtools-features\">\n @for (feature of features; track feature.type) {\n <li class=\"et-query-devtools-feature\">\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature.type) }}</span>\n <!-- Always rendered: the list is one grid, so a feature without options still needs its cell\n for the next row's chip to stay in the same column. -->\n <span class=\"et-query-devtools-feature-details\">\n @for (detail of feature.details; track detail.label) {\n <span class=\"et-query-devtools-feature-detail\">\n <span class=\"et-query-devtools-feature-label\">{{ detail.label }}</span>\n <span class=\"et-query-devtools-feature-value\">{{ detail.value }}</span>\n </span>\n }\n </span>\n </li>\n }\n </ul>\n </ng-template>\n\n <!-- Reusable inner-query list (stacks) \u2014 clicking opens the split-view drawer, not a tab switch. -->\n <ng-template #innerQueriesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track $index) {\n <button\n [class.et-query-devtools-inner-row--active]=\"stackSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"stackSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: link.segments, fallback: link.query.id() || 'query ' + ($index + 1) }\n \"\n />\n </span>\n @let linkStats = linkActivity(link);\n @if (linkStats.hasActivity) {\n <span class=\"et-query-devtools-traffic\">\n {{ formatTransferred(linkStats.stats.receivedBytes, linkStats.stats.hasEstimatedBytes) }}\n </span>\n }\n </button>\n }\n </div>\n </ng-template>\n\n <!-- The queries a form feeds \u2014 clicking one opens the split-view drawer, not a tab switch. -->\n <ng-template #formDrivesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track link.id) {\n <button\n [class.et-query-devtools-inner-row--active]=\"formSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"formSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: link.segments, fallback: link.query.id() || 'query' }\"\n />\n </span>\n </button>\n }\n </div>\n </ng-template>\n\n <!-- Reusable query detail \u2014 used by the Queries tab and by every split-view drawer. -->\n <ng-template #queryDetailTpl let-sel=\"sel\">\n <div class=\"et-query-devtools-detail-head\">\n <span class=\"et-query-devtools-method\">{{ sel.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: routeSegments(sel.entry, sel.query), fallback: '\u2014' }\"\n />\n </span>\n </div>\n\n @if (requestProgress(sel.query); as live) {\n <div class=\"et-query-devtools-live\">\n @if (live.retry; as retry) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"The retry policy is backing off before it tries again. A query stuck loading with this chip is waiting, not slow.\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n attempt {{ retry.attempt }} in {{ formatCountdown(live.retryInMs) }}\n </span>\n <span class=\"et-query-devtools-live-note\">\n {{ retryCause(retry.status) }} \u00B7 backing off {{ formatDuration(retry.delayMs) }}\n </span>\n } @else if (live.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"How many HTTP attempts the last execution took - the retry policy fired for the rest\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ live.attempts }} attempts\n </span>\n }\n\n @if (live.progress; as progress) {\n <span\n [attr.aria-valuenow]=\"progress.percentage\"\n class=\"et-query-devtools-progress\"\n role=\"progressbar\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-label=\"Transfer progress\"\n >\n <span [style.inline-size.%]=\"progress.percentage\" class=\"et-query-devtools-progress-bar\"></span>\n </span>\n <span class=\"et-query-devtools-live-note\">\n {{ formatPercent(progress.percentage) }} \u00B7 {{ formatBytes(progress.loaded) }} of\n {{ formatBytes(progress.total) }}\n @if (progress.remainingTime !== null) {\n \u00B7 {{ formatDuration(progress.remainingTime) }} left\n }\n </span>\n }\n </div>\n }\n\n <div class=\"et-query-devtools-actions\">\n <span class=\"et-query-devtools-actions-label\">Run</span>\n <button (click)=\"executeQuery(sel.query, false)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"executeQuery(sel.query, true)\" class=\"et-query-devtools-btn\" type=\"button\">Cached</button>\n <button (click)=\"resetQuery(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Reset</button>\n <button (click)=\"copyReport(sel.entry, sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">\n {{ copiedReport() ? '\u2713 Copied' : 'Copy report' }}\n </button>\n <button\n (click)=\"copyInsomniaRequest(sel.entry, sel.query)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Copy as an Insomnia collection (Import \u2192 From Clipboard). Includes the request headers, and the token refresh for a secure query.\"\n >\n {{ copiedInsomnia() ? '\u2713 Copied' : 'Insomnia' }}\n </button>\n <span class=\"et-query-devtools-actions-label\">Edit</span>\n <button (click)=\"openResponseEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Response</button>\n <button (click)=\"openArgsEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Replay args</button>\n <span class=\"et-query-devtools-actions-label\">Force</span>\n <button (click)=\"forceLoading(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Loading</button>\n <button (click)=\"forceError(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Error</button>\n <button (click)=\"forceEmpty(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Empty</button>\n <button (click)=\"clearForced(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n\n @if (editorMode() !== 'none') {\n <div class=\"et-query-devtools-editor\">\n @if (editorMode() === 'response') {\n <h4 class=\"et-query-devtools-section\">Edit response \u2192 setResponse()</h4>\n <textarea\n #respArea\n [value]=\"editorSeed\"\n (input)=\"responseDraft.set(respArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyResponse(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Apply</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n } @else {\n <h4 class=\"et-query-devtools-section\">Replay with edited args \u2192 execute()</h4>\n <textarea\n #argsArea\n [value]=\"editorSeed\"\n (input)=\"argsDraft.set(argsArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyArgs(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n }\n @if (editError(); as err) {\n <p class=\"et-query-devtools-editor-error\">{{ err }}</p>\n }\n </div>\n }\n\n @let detailRuns = queryRuns(sel.entry);\n <nav class=\"et-query-devtools-subtabs\" role=\"tablist\">\n @for (tab of detailTabs; track tab.id) {\n <button\n [class.et-query-devtools-subtab--active]=\"detailTab() === tab.id\"\n [attr.aria-selected]=\"detailTab() === tab.id\"\n (click)=\"detailTab.set(tab.id)\"\n class=\"et-query-devtools-subtab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n @if (tab.id === 'history' && detailRuns.length) {\n <span class=\"et-query-devtools-tab-badge\">{{ detailRuns.length }}</span>\n }\n <!-- The error body lives under Data, so the tab has to say so - a failure must not hide\n behind a tab that is not open. -->\n @if (tab.id === 'data' && sel.query.error()) {\n <span class=\"et-query-devtools-tab-badge et-query-devtools-tab-badge--error\">!</span>\n }\n </button>\n }\n </nav>\n\n @switch (detailTab()) {\n @case ('overview') {\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">\n {{ sel.entry.meta.clientBaseUrl || sel.entry.meta.clientName || '\u2014' }}\n </dd>\n <dt>Route</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.entry.meta.route || '\u2014' }}</dd>\n <dt>Request URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ requestUrl(sel.query) || '\u2014' }}</dd>\n <dt>Status</dt>\n <dd>{{ queryStatus(sel.query) }}</dd>\n <dt>Cache key</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.query.id() || '\u2014' }}</dd>\n <dt>Last executed</dt>\n <dd>{{ formatTime(sel.query.lastTimeExecutedAt()) }}</dd>\n <dt>Triggered by</dt>\n <dd>{{ sel.query.triggeredBy() || 'user' }}</dd>\n @let refreshes = refreshesFor(sel.entry.id);\n @if (refreshes.length) {\n <dt\n title=\"Invalidations and mutation refreshes that re-executed this query, as far back as the event log goes\"\n >\n Refetched by\n </dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (refresh of refreshes.slice(0, 5); track refresh.id) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">\n {{ formatTime(refresh.timestamp) }} \u00B7 {{ refresh.label }}\n </span>\n }\n @if (refreshes.length > 5) {\n <span class=\"et-query-devtools-hint\">+{{ refreshes.length - 5 }} earlier</span>\n }\n </div>\n </dd>\n }\n @let drivers = formsDrivingQuery(sel.entry);\n @if (drivers.length) {\n <dt>Args from</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (driver of drivers; track driver.id) {\n <button\n (click)=\"selectForm(driver.id)\"\n class=\"et-query-devtools-chip et-query-devtools-chip--button\"\n type=\"button\"\n title=\"Open the query form in the Forms tab\"\n >\n {{ driver.meta.name }}\n </button>\n }\n </div>\n </dd>\n }\n @if (sel.entry.meta.features?.length) {\n <dt>Features</dt>\n <dd>\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: sel.entry.meta.features }\" />\n </dd>\n }\n </dl>\n\n @let activity = queryActivity(sel.entry);\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Activity</h4>\n <button\n [disabled]=\"!activity.hasActivity\"\n (click)=\"resetStats(sel.entry)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Clear the counters and the run history, so the next interaction can be measured on its own\"\n >\n \u27F2 Reset\n </button>\n </div>\n\n <dl class=\"et-query-devtools-stats\">\n <div class=\"et-query-devtools-stat\" title=\"Every time the query ran, cache hits included\">\n <dt>Refreshes</dt>\n <dd>{{ activity.stats.executions }}</dd>\n @if (activity.cacheServed) {\n <span class=\"et-query-devtools-stat-sub\">{{ activity.cacheServed }} without a request</span>\n }\n </div>\n <div class=\"et-query-devtools-stat\" title=\"Executions that started a request, and how they ended\">\n <dt>Requests</dt>\n <dd>{{ activity.stats.requests }}</dd>\n <span class=\"et-query-devtools-stat-sub\">\n {{ activity.stats.responses }} ok\n @if (activity.stats.errors) {\n \u00B7 {{ activity.stats.errors }} failed\n }\n </span>\n </div>\n <div\n class=\"et-query-devtools-stat\"\n title=\"Total response payload received. Sizes without a content-length header are measured from the decoded body, so they ignore compression.\"\n >\n <dt>Received</dt>\n <dd>{{ formatTransferred(activity.stats.receivedBytes, activity.stats.hasEstimatedBytes) }}</dd>\n @if (activity.avgResponseBytes !== null) {\n <span class=\"et-query-devtools-stat-sub\"\n >{{ formatBytes(activity.avgResponseBytes) }} per response</span\n >\n }\n </div>\n @if (activity.stats.sentBytes) {\n <div class=\"et-query-devtools-stat\" title=\"Total request body sent (headers not counted)\">\n <dt>Sent</dt>\n <dd>{{ formatTransferred(activity.stats.sentBytes, activity.stats.hasEstimatedBytes) }}</dd>\n </div>\n }\n @if (activity.stats.retries) {\n <div\n class=\"et-query-devtools-stat\"\n title=\"Attempts the retry policy added on top of the first one. Which run they belong to is under History.\"\n >\n <dt>Retries</dt>\n <dd>{{ activity.stats.retries }}</dd>\n </div>\n }\n <div class=\"et-query-devtools-stat\" title=\"Measured from the execution that preceded each response\">\n <dt>Duration</dt>\n <dd>{{ formatDuration(activity.stats.lastDurationMs) }}</dd>\n @if (activity.avgDurationMs !== null) {\n <span class=\"et-query-devtools-stat-sub\">{{ formatDuration(activity.avgDurationMs) }} on average</span>\n }\n </div>\n <div class=\"et-query-devtools-stat\">\n <dt>Last response</dt>\n <dd class=\"et-query-devtools-stat-value--sm\">{{ formatTime(activity.stats.lastResponseAt) }}</dd>\n @if (activity.stats.firstExecutedAt !== null) {\n <span class=\"et-query-devtools-stat-sub\"\n >first ran {{ formatTime(activity.stats.firstExecutedAt) }}</span\n >\n }\n </div>\n </dl>\n }\n @case ('history') {\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Runs</h4>\n <button\n [disabled]=\"!detailRuns.length\"\n (click)=\"resetStats(sel.entry)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Clear the run history and the counters, so the next interaction can be measured on its own\"\n >\n \u27F2 Reset\n </button>\n </div>\n @if (detailRuns.length) {\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Run</th>\n <th>Started</th>\n <th>Duration</th>\n <th>Received</th>\n <th>Diff</th>\n </tr>\n </thead>\n <tbody>\n @for (run of detailRuns; track run.index) {\n <tr [attr.data-status]=\"run.status === 'error' ? 'error' : null\">\n <td class=\"et-query-devtools-run-cell\">\n <span\n [attr.data-status]=\"runStatus(run)\"\n [title]=\"run.status\"\n class=\"et-query-devtools-status\"\n ></span>\n #{{ run.index }}\n @if (run.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Took {{\n run.attempts\n }} attempts - the duration next to it covers every one of them, backoff included\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ run.attempts }}\n </span>\n }\n @if (!run.didRequest) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Arrived without a request of this query's own - a poll, another consumer of the same cache entry, or another tab\"\n >\n shared\n </span>\n }\n </td>\n <td>{{ formatTime(run.startedAt) }}</td>\n <td>\n @if (run.endedAt === null) {\n in flight\n } @else {\n {{ formatDuration(run.endedAt - run.startedAt) }}\n }\n </td>\n <td>{{ run.receivedBytes ? formatBytes(run.receivedBytes) : '\u2014' }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n @if (canDiffRun(sel.entry, run)) {\n <button\n [class.et-query-devtools-btn--active]=\"diffRunIndex() === run.index\"\n (click)=\"toggleRunDiff(run)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Compare this response against the previous one still held\"\n >\n Diff\n </button>\n } @else if (run.hasResponse) {\n <span class=\"et-query-devtools-count\">nothing earlier to diff</span>\n } @else if (run.status === 'success') {\n <span class=\"et-query-devtools-count\">body no longer held</span>\n } @else {\n <span class=\"et-query-devtools-count\">\u2014</span>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n\n @if (responseDiff(sel.entry); as comparison) {\n <div class=\"et-query-devtools-section-row\">\n <h5 class=\"et-query-devtools-section\">\n Response diff \u00B7 run #{{ comparison.before.index }} \u2192 #{{ comparison.after.index }}\n </h5>\n <span class=\"et-query-devtools-count\">\n @if (comparison.diff.entries.length) {\n {{ comparison.diff.entries.length }} path{{ comparison.diff.entries.length === 1 ? '' : 's' }}\n @if (comparison.diff.truncated) {\n (capped)\n }\n } @else {\n identical\n }\n </span>\n </div>\n @if (comparison.diff.entries.length) {\n <table class=\"et-query-devtools-table et-query-devtools-diff\">\n <thead>\n <tr>\n <th>Path</th>\n <th>Before</th>\n <th>After</th>\n </tr>\n </thead>\n <tbody>\n @for (change of comparison.diff.entries; track change.path) {\n <tr [attr.data-kind]=\"change.kind\">\n <td class=\"et-query-devtools-mono\">{{ change.path }}</td>\n <td class=\"et-query-devtools-mono et-query-devtools-diff-before\">\n {{ change.kind === 'added' ? '\u2014' : inlineValue(change.before) }}\n </td>\n <td class=\"et-query-devtools-mono et-query-devtools-diff-after\">\n {{ change.kind === 'removed' ? '\u2014' : inlineValue(change.after) }}\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <p class=\"et-query-devtools-empty\">The two responses are identical.</p>\n }\n }\n } @else {\n <p class=\"et-query-devtools-empty\">No runs recorded yet.</p>\n }\n }\n @case ('data') {\n @if (sel.entry.meta.gqlQuery; as gqlQuery) {\n <div class=\"et-query-devtools-section-row\">\n <h5 class=\"et-query-devtools-section\">GraphQL document</h5>\n <button\n (click)=\"copyGqlDocument(gqlQuery)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Copy the GraphQL document\"\n >\n {{ copiedGql() ? '\u2713 Copied' : '\u29C9 Copy' }}\n </button>\n </div>\n <pre class=\"et-query-devtools-code\">{{ gqlDocument(gqlQuery) }}</pre>\n }\n\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Value explorer</h4>\n <input\n #jsonSearchInput\n [value]=\"jsonSearch()\"\n (input)=\"jsonSearch.set(jsonSearchInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter keys / values\u2026\"\n />\n </div>\n\n <h5 class=\"et-query-devtools-section\">Args</h5>\n <et-query-devtools-json\n [value]=\"queryArgs(sel.query)\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"args\"\n />\n\n @if (sel.query.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Error ({{ error.raw.status }})</h5>\n <et-query-devtools-json\n [value]=\"error.isList ? error.errors : error.error\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"error\"\n />\n } @else {\n <h5 class=\"et-query-devtools-section\">Response</h5>\n <et-query-devtools-json\n [value]=\"sel.query.response()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"response\"\n />\n }\n }\n }\n </ng-template>\n </section>\n}\n\n@if (!open()) {\n <et-query-devtools-toggle (openChange)=\"toggleOpen()\" />\n}\n\n@if (inspectActive() && inspectHover(); as hover) {\n <div\n [style.left.px]=\"hover.rect.left\"\n [style.top.px]=\"hover.rect.top\"\n [style.width.px]=\"hover.rect.width\"\n [style.height.px]=\"hover.rect.height\"\n class=\"et-query-devtools-inspect-box\"\n >\n <span class=\"et-query-devtools-inspect-tag\">{{ inspectLabel(hover.entries) }}</span>\n </div>\n}\n", styles: ["@layer components{.et-query-devtools-host{--_et-qdt-bg: var(--et-surface-background-solid, rgb(24 24 27));--_et-qdt-fg: var(--et-surface-color-solid, rgb(244 244 245));--_et-qdt-muted: var(--et-surface-color-muted-solid, rgb(161 161 170));--_et-qdt-subtle: var(--et-surface-color-subtle-solid, rgb(113 113 122));--_et-qdt-border: var(--et-surface-border-solid, rgb(255 255 255 / .12));--_et-qdt-interaction: var(--et-surface-interaction-solid, rgb(255 255 255));--_et-qdt-accent: var(--et-theme-color-primary-solid, rgb(96 165 250));--_et-qdt-hover: color-mix(in srgb, var(--_et-qdt-interaction) 10%, transparent);--_et-qdt-active: color-mix(in srgb, var(--_et-qdt-interaction) 18%, transparent);--_et-qdt-success: rgb(34 197 94);--_et-qdt-error: rgb(239 68 68);--_et-qdt-loading: rgb(234 179 8);position:fixed;inset-inline:0;inset-block-end:0;z-index:2147483000;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,Liberation Mono,monospace;font-size:1.2rem;line-height:1.4;pointer-events:none}.et-query-devtools-host>*{pointer-events:auto}.et-query-devtools-panel{position:relative;display:flex;flex-direction:column;inline-size:100%;min-block-size:20rem;max-block-size:90vh;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border-block-start:1px solid var(--_et-qdt-border);box-shadow:0 -.4rem 2rem #00000059}.et-query-devtools-panel--resizing{-webkit-user-select:none;user-select:none}.et-query-devtools-resize{position:absolute;inset-inline:0;inset-block-start:0;block-size:.6rem;cursor:ns-resize;z-index:1}.et-query-devtools-resize:after{content:\"\";position:absolute;inset-inline:50%;inset-block-start:.2rem;inline-size:4rem;margin-inline-start:-2rem;block-size:.2rem;border-radius:999px;background:var(--_et-qdt-border)}.et-query-devtools-resize:hover:after{background:var(--_et-qdt-accent)}.et-query-devtools-header{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding-inline:.8rem;border-block-end:1px solid var(--_et-qdt-border);flex:0 0 auto}.et-query-devtools-armed-bar{display:flex;align-items:center;gap:.8rem;flex:0 0 auto;padding:.6rem .8rem;background:color-mix(in srgb,var(--_et-qdt-error) 14%,transparent);border-block-end:1px solid color-mix(in srgb,var(--_et-qdt-error) 45%,transparent);color:var(--_et-qdt-error);font-size:1.1rem}.et-query-devtools-armed-text{flex:1 1 auto;min-inline-size:0}.et-query-devtools-armed-dot{flex:0 0 auto;inline-size:.8rem;block-size:.8rem;border-radius:50%;background:var(--_et-qdt-error);animation:et-query-devtools-armed-pulse 1.6s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.et-query-devtools-armed-dot{animation:none}}@keyframes et-query-devtools-armed-pulse{50%{opacity:.25}}.et-query-devtools-armed-bar .et-query-devtools-btn{flex:0 0 auto;border-color:color-mix(in srgb,var(--_et-qdt-error) 55%,transparent);color:var(--_et-qdt-error)}.et-query-devtools-tabs{display:flex;flex-wrap:wrap;gap:.2rem}.et-query-devtools-tab{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);padding:.9rem 1rem;cursor:pointer;border-block-end:2px solid transparent;font:inherit}.et-query-devtools-tab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-tab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-tab-badge{display:inline-block;margin-inline-start:.5rem;padding-inline:.45rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-interaction) 12%,transparent);color:var(--_et-qdt-muted);font-size:1rem;font-variant-numeric:tabular-nums}.et-query-devtools-tab-badge--error{background:color-mix(in srgb,var(--_et-qdt-error) 25%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-header-actions{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}.et-query-devtools-inspect-btn{appearance:none;position:relative;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.4rem .8rem;font:inherit;transition:color .12s ease,background-color .12s ease,border-color .12s ease}.et-query-devtools-inspect-btn:hover{color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent)}.et-query-devtools-inspect-btn:focus-visible{outline:2px solid var(--_et-qdt-accent);outline-offset:2px}.et-query-devtools-inspect-icon{inline-size:1.4rem;block-size:1.4rem;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round}.et-query-devtools-inspect-icon-dot{fill:currentColor;stroke:none}.et-query-devtools-inspect-btn--active,.et-query-devtools-inspect-btn--active:hover{color:#09090b;background:var(--_et-qdt-accent);border-color:var(--_et-qdt-accent)}.et-query-devtools-inspect-btn--active:after{content:\"\";position:absolute;inset:-.3rem;border:1px solid var(--_et-qdt-accent);border-radius:.6rem;pointer-events:none;animation:et-query-devtools-inspect-pulse 1.4s ease-in-out infinite}.et-query-devtools-inspect-btn--active .et-query-devtools-inspect-icon-ring{animation:et-query-devtools-inspect-pulse 1.4s ease-in-out infinite}@keyframes et-query-devtools-inspect-pulse{0%,to{opacity:.2}50%{opacity:1}}@media(prefers-reduced-motion:reduce){.et-query-devtools-inspect-btn{transition:none}.et-query-devtools-inspect-btn--active:after,.et-query-devtools-inspect-btn--active .et-query-devtools-inspect-icon-ring{animation:none;opacity:1}}.et-query-devtools-close{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.6rem;font:inherit}.et-query-devtools-close-panel{appearance:none;display:inline-flex;align-items:center;gap:.6rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:var(--_et-qdt-hover);color:var(--_et-qdt-fg);cursor:pointer;padding:.4rem .6rem .4rem .8rem;font:inherit}.et-query-devtools-close-panel:hover{border-color:var(--_et-qdt-error);color:var(--_et-qdt-error)}.et-query-devtools-kbd{border-radius:.3rem;background:var(--_et-qdt-hover);box-shadow:inset 0 0 0 1px var(--_et-qdt-border);color:var(--_et-qdt-muted);padding:.15rem .4rem;font:inherit;font-size:1.1rem;line-height:1.2;white-space:nowrap}.et-query-devtools-kbd--inverse{background:#00000026;box-shadow:inset 0 0 0 1px #00000040;color:inherit}.et-query-devtools-inspect-box{position:fixed;z-index:2147483002;pointer-events:none;border:2px solid var(--_et-qdt-accent);background:color-mix(in srgb,var(--_et-qdt-accent) 15%,transparent);border-radius:.2rem}.et-query-devtools-inspect-tag{position:absolute;inset-block-start:100%;inset-inline-start:0;margin-block-start:.2rem;padding:.2rem .5rem;border-radius:.3rem;background:var(--_et-qdt-accent);color:#09090b;font-family:ui-monospace,monospace;font-size:1.1rem;font-weight:600;white-space:nowrap}.et-query-devtools-close:hover{color:var(--_et-qdt-fg)}.et-query-devtools-body{flex:1 1 auto;min-block-size:0;overflow:hidden}.et-query-devtools-master-detail{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-list{flex:0 0 34%;min-inline-size:22rem;max-inline-size:40rem;overflow-y:auto;border-inline-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail{flex:1 1 auto;min-inline-size:0;overflow-y:auto;padding:1rem}.et-query-devtools-detail .et-query-devtools-detail-head,.et-query-devtools-drawer .et-query-devtools-detail-head{padding-block-end:.8rem;border-block-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail .et-query-devtools-meta,.et-query-devtools-drawer .et-query-devtools-meta{padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-detail .et-query-devtools-section-row,.et-query-devtools-drawer .et-query-devtools-section-row{padding-block-start:.8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-filter-chip{display:inline-flex;align-items:center;gap:.6rem;padding:.3rem .35rem .3rem .7rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-accent) 16%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_et-qdt-accent) 45%,transparent);color:var(--_et-qdt-accent);font-size:1.1rem;font-weight:600;letter-spacing:.02em}.et-query-devtools-filter-icon{font-size:1.3rem;line-height:1}.et-query-devtools-filter-count{border-radius:999px;background:var(--_et-qdt-accent);color:#09090b;min-inline-size:1.8rem;padding:0 .5rem;text-align:center}.et-query-devtools-filter-clear{appearance:none;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.3rem .6rem;font:inherit;font-size:1.15rem;line-height:1.4}.et-query-devtools-filter-clear:hover{border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-scroll{block-size:100%;overflow:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.et-query-devtools-split{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-split .et-query-devtools-scroll{flex:1 1 auto;min-inline-size:0}.et-query-devtools-pane{display:flex;flex-direction:column;flex:1 1 auto;block-size:100%;min-inline-size:0;min-block-size:0;overflow:hidden}.et-query-devtools-pane>.et-query-devtools-scroll{padding-block:0 1rem}.et-query-devtools-drawer{flex:0 0 42%;min-inline-size:26rem;max-inline-size:48rem;overflow-y:auto;padding:0 1rem 1rem;border-inline-start:1px solid var(--_et-qdt-border)}.et-query-devtools-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem 0;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg);color:var(--_et-qdt-muted);border-block-end:1px solid var(--_et-qdt-border);margin-block-end:.8rem}.et-query-devtools-toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.8rem;padding:.8rem;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-toolbar-row{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 100%;gap:.8rem}.et-query-devtools-toolbar-row .et-query-devtools-search{flex:1 1 100%}.et-query-devtools-toolbar--filtered{background:linear-gradient(to right,color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent),transparent 65%),var(--_et-qdt-bg);box-shadow:inset .2rem 0 0 var(--_et-qdt-accent),inset 0 -1px color-mix(in srgb,var(--_et-qdt-accent) 30%,transparent)}.et-query-devtools-field{display:inline-flex;align-items:center;gap:.6rem;color:var(--_et-qdt-muted)}.et-query-devtools-select{background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-count{color:var(--_et-qdt-subtle)}.et-query-devtools-row{display:flex;align-items:center;gap:.8rem;inline-size:100%;min-block-size:3.6rem;text-align:start;appearance:none;border:none;border-block-end:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-fg);padding:.8rem;cursor:pointer;font:inherit}.et-query-devtools-row>.et-query-devtools-chip{padding-block:.1rem;line-height:1.2}.et-query-devtools-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-row--active{background:var(--_et-qdt-active)}.et-query-devtools-method{font-weight:700;font-size:1.1rem;color:var(--_et-qdt-muted);white-space:nowrap}.et-query-devtools-route{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-inline-size:0}.et-query-devtools-route-param{color:var(--_et-qdt-accent)}.et-query-devtools-route-query{color:var(--_et-qdt-muted)}.et-query-devtools-status{inline-size:.8rem;block-size:.8rem;border-radius:999px;flex:0 0 auto;background:var(--_et-qdt-subtle)}.et-query-devtools-status[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-status[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-status[data-status=loading],.et-query-devtools-status[data-status=running]{background:var(--_et-qdt-loading)}.et-query-devtools-detail-head{display:flex;align-items:center;gap:.8rem;margin-block-end:.8rem}.et-query-devtools-live{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-live-note{color:var(--_et-qdt-muted);font-size:1.15rem}.et-query-devtools-progress{display:block;flex:1 1 12rem;min-inline-size:8rem;max-inline-size:24rem;block-size:.5rem;border-radius:999px;overflow:hidden;background:var(--_et-qdt-border)}.et-query-devtools-progress-bar{display:block;block-size:100%;border-radius:inherit;background:var(--_et-qdt-accent)}.et-query-devtools-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-block-end:.8rem}.et-query-devtools-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.3rem;background:transparent;color:var(--_et-qdt-fg);padding:.25rem .55rem;font-size:1.15rem;line-height:1.4;cursor:pointer}.et-query-devtools-btn:hover{background:var(--_et-qdt-hover)}.et-query-devtools-btn:disabled{color:var(--_et-qdt-subtle);cursor:default;background:transparent}.et-query-devtools-toolbar-btn{margin-inline-start:auto;flex:0 0 auto}.et-query-devtools-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;margin:0 0 1rem}.et-query-devtools-meta dt{color:var(--_et-qdt-muted)}.et-query-devtools-meta dd{margin:0;overflow-wrap:anywhere}.et-query-devtools-section{margin:1rem 0 .4rem;font-size:1.1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:.6rem;margin:0 0 1rem}.et-query-devtools-stat{padding:.6rem .8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-stat dt{font-size:1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-stat dd{margin:0;font-size:1.5rem;font-variant-numeric:tabular-nums}.et-query-devtools-stat-value--sm{font-size:1.2rem}.et-query-devtools-stat-sub{display:block;font-size:1rem;color:var(--_et-qdt-subtle)}.et-query-devtools-traffic{margin-inline-start:auto;flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--_et-qdt-subtle)}.et-query-devtools-json{margin:0;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere;max-block-size:24rem;overflow-y:auto}.et-query-devtools-json--error{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-mono{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-code{margin:0 0 .8rem;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1.15rem;max-block-size:20rem}.et-query-devtools-ws-message{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;margin-block-end:.4rem}.et-query-devtools-url{display:inline-block;max-inline-size:32rem;margin-inline-start:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.et-query-devtools-chips,.et-query-devtools-chip-row{display:flex;flex-wrap:wrap;gap:.4rem;margin-block-end:.8rem}.et-query-devtools-features{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:center;gap:.4rem .8rem;margin:0 0 .8rem;padding:0;list-style:none}.et-query-devtools-meta .et-query-devtools-features{margin-block-end:0}.et-query-devtools-feature{display:contents}.et-query-devtools-feature>.et-query-devtools-chip{justify-self:start}.et-query-devtools-feature-details{display:flex;flex-wrap:wrap;gap:.3rem}.et-query-devtools-feature-detail{display:inline-flex;align-items:baseline;gap:.4rem;padding:.2rem .5rem;border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 5%,transparent);font-size:1.1rem}.et-query-devtools-feature-label{color:var(--_et-qdt-muted)}.et-query-devtools-feature-value{color:var(--_et-qdt-fg);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-chip{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .6rem;border-radius:999px;border:1px solid var(--_et-qdt-border);font-size:1.1rem;color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-interaction) 6%,transparent)}.et-query-devtools-chip--muted{color:var(--_et-qdt-muted)}.et-query-devtools-retry-glyph{font-size:.7em;line-height:1}.et-query-devtools-chip--loading{color:var(--_et-qdt-loading);border-color:color-mix(in srgb,var(--_et-qdt-loading) 50%,transparent)}.et-query-devtools-chip--error{color:var(--_et-qdt-error);border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-chip--success{color:var(--_et-qdt-success);border-color:color-mix(in srgb,var(--_et-qdt-success) 50%,transparent)}button.et-query-devtools-chip--button{appearance:none;cursor:pointer;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}button.et-query-devtools-chip--button:hover{border-color:var(--_et-qdt-accent);color:var(--_et-qdt-accent)}.et-query-devtools-caret{display:inline-block;width:1.2rem;color:var(--_et-qdt-muted)}.et-query-devtools-facet{appearance:none;cursor:pointer;color:var(--_et-qdt-muted);font:inherit;font-size:1.1rem;line-height:1.4}.et-query-devtools-facet:hover:not(:disabled){color:var(--_et-qdt-fg);border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent)}.et-query-devtools-facet:disabled{color:var(--_et-qdt-subtle);cursor:default}.et-query-devtools-facet--active{color:var(--_et-qdt-accent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-accent) 14%,transparent)}.et-query-devtools-facet-count{font-variant-numeric:tabular-nums;color:inherit;opacity:.75}.et-query-devtools-card{border:1px solid var(--_et-qdt-border);border-radius:.6rem;padding:1rem}.et-query-devtools-card-head{display:flex;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-card-title{display:flex;align-items:baseline;gap:.5rem;margin-block-end:.2rem}.et-query-devtools-card-sub{color:var(--_et-qdt-subtle);font-size:1.1rem;margin-block-end:.6rem;overflow-wrap:anywhere}.et-query-devtools-inner-queries{display:flex;flex-direction:column;gap:.3rem}.et-query-devtools-inner-row{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-fg);padding:.4rem .6rem;cursor:pointer;font:inherit}.et-query-devtools-inner-row:hover:not([disabled]){background:var(--_et-qdt-hover)}.et-query-devtools-inner-row--active{background:var(--_et-qdt-active);border-color:var(--_et-qdt-accent)}.et-query-devtools-inner-row[disabled]{cursor:default;opacity:.6}.et-query-devtools-step-list{display:flex;flex-direction:column;gap:.4rem;margin:0;padding:0;list-style:none}.et-query-devtools-step-item{border:1px solid var(--_et-qdt-border);border-radius:.4rem;overflow:hidden}.et-query-devtools-step-item[data-status=error]{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-step-head{display:flex;align-items:stretch}.et-query-devtools-step-head .et-query-devtools-inner-row{border:none;border-radius:0;flex:1 1 auto}.et-query-devtools-step-caret-btn{appearance:none;border:none;border-inline-start:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:0 .7rem;font:inherit}.et-query-devtools-step-caret-btn:hover{background:var(--_et-qdt-hover);color:var(--_et-qdt-fg)}.et-query-devtools-step-toggle{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:none;background:transparent;color:var(--_et-qdt-fg);padding:.6rem;cursor:pointer;font:inherit}.et-query-devtools-step-toggle:hover{background:var(--_et-qdt-hover)}.et-query-devtools-step-index{flex:1 1 auto;min-inline-size:0}.et-query-devtools-step-caret{color:var(--_et-qdt-muted)}.et-query-devtools-step-io{padding:0 .8rem .8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-table{inline-size:100%;border-collapse:collapse}.et-query-devtools-table th,.et-query-devtools-table td{text-align:start;padding:.5rem .6rem;border-block-end:1px solid var(--_et-qdt-border);vertical-align:top}.et-query-devtools-table th{color:var(--_et-qdt-muted);font-weight:600;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-table tr[data-status=error] td{color:var(--_et-qdt-error)}.et-query-devtools-empty{color:var(--_et-qdt-subtle);padding:1.2rem;text-align:center}.et-query-devtools-actions-label{color:var(--_et-qdt-subtle);align-self:center;text-transform:uppercase;font-size:1rem;letter-spacing:.05em}.et-query-devtools-actions-label:not(:first-child){margin-inline-start:.7rem}.et-query-devtools-cell-actions{display:flex;gap:.4rem;white-space:nowrap}.et-query-devtools-section-row{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-block-start:1rem}.et-query-devtools-search{flex:0 1 20rem;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-row-link{display:inline-flex;align-items:center;gap:.6rem;appearance:none;border:0;background:none;padding:0;color:inherit;font:inherit;text-align:start;cursor:pointer}.et-query-devtools-row-link:hover .et-query-devtools-url,.et-query-devtools-row-link:focus-visible .et-query-devtools-url{color:var(--_et-qdt-accent);text-decoration:underline}.et-query-devtools-hint{flex:1 1 24rem;color:var(--_et-qdt-muted);font-size:1.1rem;margin:0}.et-query-devtools-card--armed{border-color:color-mix(in srgb,var(--_et-qdt-error) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-error) 6%,transparent)}.et-query-devtools-faults{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));gap:.8rem 1.6rem}.et-query-devtools-faults .et-query-devtools-field{display:grid;grid-template-columns:8rem minmax(0,1fr);align-items:baseline;row-gap:.2rem;color:var(--_et-qdt-fg)}.et-query-devtools-faults .et-query-devtools-field small{grid-column:2;color:var(--_et-qdt-subtle);font-size:1.1rem}.et-query-devtools-input{inline-size:100%;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-input--number{inline-size:8rem;font-variant-numeric:tabular-nums}.et-query-devtools-editor{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.8rem;margin-block-end:1rem}.et-query-devtools-textarea{inline-size:100%;min-block-size:12rem;resize:vertical;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;font-family:inherit;font-size:1.2rem}.et-query-devtools-editor-error{color:var(--_et-qdt-error);margin:.4rem 0 0}.et-query-devtools-json-node,.et-query-devtools-json-children{display:flex;flex-direction:column}.et-query-devtools-json-children{padding-inline-start:1.4rem;border-inline-start:1px solid var(--_et-qdt-border);margin-inline-start:.4rem}.et-query-devtools-json-row{display:flex;align-items:baseline;gap:.4rem;padding:.1rem 0;font-family:inherit;line-height:1.5}.et-query-devtools-json-toggle{appearance:none;display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;border:none;background:transparent;color:var(--_et-qdt-fg);cursor:pointer;text-align:start;font:inherit;padding:.1rem 0}.et-query-devtools-json-node>.et-query-devtools-json-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-json-caret{color:var(--_et-qdt-subtle);inline-size:1rem;flex:0 0 auto}.et-query-devtools-json-key{color:var(--_et-qdt-accent)}.et-query-devtools-json-colon,.et-query-devtools-json-preview{color:var(--_et-qdt-subtle)}.et-query-devtools-json-chunk{color:var(--_et-qdt-fg);opacity:.75;font-variant-numeric:tabular-nums}.et-query-devtools-json-value{overflow-wrap:anywhere}.et-query-devtools-json-leaf[data-kind=string] .et-query-devtools-json-value{color:var(--_et-qdt-success)}.et-query-devtools-json-leaf[data-kind=number] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=boolean] .et-query-devtools-json-value{color:var(--_et-qdt-loading)}.et-query-devtools-json-leaf[data-kind=null] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=undefined] .et-query-devtools-json-value{color:var(--_et-qdt-subtle)}.et-query-devtools-json-hit{background:color-mix(in srgb,var(--_et-qdt-loading) 40%,transparent);border-radius:.2rem}.et-query-devtools-json-copy{appearance:none;display:inline-flex;align-items:center;justify-content:center;align-self:center;flex:0 0 auto;inline-size:2.2rem;block-size:2.2rem;margin-block:-.4rem;border:none;border-radius:.3rem;background:transparent;color:var(--_et-qdt-subtle);cursor:pointer;opacity:0;font:inherit;font-size:1.3rem;line-height:1}.et-query-devtools-json-copy:hover:not(.et-query-devtools-json-copy--copied){background:var(--_et-qdt-active);color:var(--_et-qdt-fg)}.et-query-devtools-json-row:hover .et-query-devtools-json-copy,.et-query-devtools-json-copy:focus-visible,.et-query-devtools-json-copy--copied{opacity:1}.et-query-devtools-json-copy--copied{color:var(--_et-qdt-success)}.et-query-devtools-timeline{display:grid;grid-template-columns:minmax(10rem,26rem) minmax(14rem,1fr) max-content max-content max-content;gap:.1rem .8rem;padding-inline:.4rem 1rem}.et-query-devtools-timeline-axis,.et-query-devtools-timeline-row{display:grid;grid-column:1 / -1;grid-template-columns:subgrid;align-items:center}.et-query-devtools-timeline-axis{position:sticky;inset-block-start:0;z-index:1;background:var(--_et-qdt-bg);border-block-end:1px solid var(--_et-qdt-border);padding-block-end:.4rem;margin-block-end:.4rem}.et-query-devtools-timeline-ticks{grid-column:2;position:relative;block-size:1.5rem}.et-query-devtools-timeline-tick{position:absolute;inset-block-start:0;transform:translate(-50%);color:var(--_et-qdt-subtle);font-size:1.1rem;white-space:nowrap}.et-query-devtools-timeline-tick:first-child{transform:none}.et-query-devtools-timeline-tick:last-child{transform:translate(-100%)}.et-query-devtools-timeline-row{appearance:none;border:0;background:transparent;color:inherit;font:inherit;text-align:start;padding:.15rem 0;cursor:pointer}.et-query-devtools-timeline-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-timeline-row--active{background:var(--_et-qdt-active)}.et-query-devtools-timeline-label{display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;overflow:hidden}.et-query-devtools-timeline-label .et-query-devtools-url{max-inline-size:none;margin-inline-start:0}.et-query-devtools-timeline-track{position:relative;block-size:1.8rem;border-radius:.2rem;overflow:hidden;background-color:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);background-image:repeating-linear-gradient(to right,var(--_et-qdt-border) 0 1px,transparent 1px 25%)}.et-query-devtools-timeline-bar{position:absolute;inset-block:.4rem;min-inline-size:.2rem;border-radius:.2rem;background:var(--_et-qdt-subtle)}.et-query-devtools-timeline-bar[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-timeline-bar[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-timeline-bar[data-status=loading]{background:var(--_et-qdt-loading)}.et-query-devtools-timeline-bar[data-status=aborted]{min-inline-size:.5rem;background:color-mix(in srgb,var(--_et-qdt-subtle) 20%,transparent);outline:1px solid color-mix(in srgb,var(--_et-qdt-subtle) 70%,transparent);outline-offset:-1px}.et-query-devtools-timeline-duration,.et-query-devtools-timeline-size{text-align:end;color:var(--_et-qdt-muted);font-size:1.1rem;font-variant-numeric:tabular-nums;white-space:nowrap}.et-query-devtools-timeline-duration{color:var(--_et-qdt-fg)}.et-query-devtools-timeline-flags{display:flex;align-items:center;gap:.3rem;white-space:nowrap}.et-query-devtools-timeline-flags .et-query-devtools-chip{padding:0 .4rem;border-color:transparent;border-radius:.3rem;background:color-mix(in srgb,currentcolor 12%,transparent);font-size:1rem;line-height:1.5;font-variant-numeric:tabular-nums}.et-query-devtools-subtabs{display:flex;flex-wrap:wrap;gap:.2rem;border-block-end:1px solid var(--_et-qdt-border);margin-block:.8rem}.et-query-devtools-subtabs+.et-query-devtools-section-row{padding-block-start:0;border-block-start:0}.et-query-devtools-subtab{appearance:none;display:inline-flex;align-items:center;gap:.4rem;border:0;border-block-end:2px solid transparent;background:transparent;color:var(--_et-qdt-muted);font:inherit;font-size:1.15rem;padding:.4rem .6rem;margin-block-end:-1px;cursor:pointer}.et-query-devtools-subtab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-subtab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-run-cell{display:flex;align-items:center;gap:.5rem}.et-query-devtools-btn--active{color:var(--_et-qdt-accent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent)}.et-query-devtools-diff td{max-inline-size:28rem;overflow-wrap:anywhere}.et-query-devtools-diff-before{color:var(--_et-qdt-muted)}.et-query-devtools-diff tr[data-kind=removed] .et-query-devtools-diff-before{color:var(--_et-qdt-error)}.et-query-devtools-diff tr[data-kind=added] .et-query-devtools-diff-after{color:var(--_et-qdt-success)}.et-query-devtools-diff tr[data-kind=changed] .et-query-devtools-diff-after{color:var(--_et-qdt-accent)}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QueryDevtoolsJsonComponent, selector: "et-query-devtools-json", inputs: ["value", "nodeKey", "depth", "search", "chunk", "path", "expandedPaths", "collapsedPaths", "toggleFn"] }, { kind: "component", type: QueryDevtoolsToggleComponent, selector: "et-query-devtools-toggle", outputs: ["openChange"] }], encapsulation: i0.ViewEncapsulation.None }); }
|
|
48129
49331
|
}
|
|
48130
49332
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: QueryDevtoolsComponent, decorators: [{
|
|
48131
49333
|
type: Component,
|
|
48132
49334
|
args: [{ selector: 'et-query-devtools', encapsulation: ViewEncapsulation.None, imports: [NgTemplateOutlet, QueryDevtoolsJsonComponent, QueryDevtoolsToggleComponent], host: {
|
|
48133
49335
|
class: 'et-query-devtools-host',
|
|
48134
|
-
}, template: "@if (open()) {\n <section\n [class.et-query-devtools-panel--resizing]=\"resizing()\"\n [style.height.px]=\"panelHeight()\"\n class=\"et-query-devtools-panel\"\n >\n <div\n (pointerdown)=\"startResize($event)\"\n class=\"et-query-devtools-resize\"\n title=\"Drag to resize\"\n aria-hidden=\"true\"\n ></div>\n <header class=\"et-query-devtools-header\">\n <nav class=\"et-query-devtools-tabs\" role=\"tablist\">\n @for (tab of tabs; track tab.id) {\n <button\n [class.et-query-devtools-tab--active]=\"activeTab() === tab.id\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n (click)=\"activeTab.set(tab.id)\"\n class=\"et-query-devtools-tab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n </button>\n }\n </nav>\n\n <div class=\"et-query-devtools-header-actions\">\n <button\n [class.et-query-devtools-inspect-btn--active]=\"inspectActive()\"\n (click)=\"toggleInspect()\"\n class=\"et-query-devtools-inspect-btn\"\n type=\"button\"\n title=\"Inspect: hover the UI to find its query\"\n >\n \u2316 Inspect\n </button>\n <button\n (click)=\"toggleOpen()\"\n class=\"et-query-devtools-close-panel\"\n type=\"button\"\n title=\"Close devtools ({{ shortcut }})\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Close\n <kbd class=\"et-query-devtools-kbd\">{{ shortcut }}</kbd>\n </button>\n </div>\n </header>\n\n <div class=\"et-query-devtools-body\">\n @switch (activeTab()) {\n <!-- QUERIES -->\n @case ('queries') {\n <div class=\"et-query-devtools-master-detail\">\n <div class=\"et-query-devtools-list\">\n <div [class.et-query-devtools-toolbar--filtered]=\"!!inspectFilterIds()\" class=\"et-query-devtools-toolbar\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n <button\n (click)=\"clearInspectFilter()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the inspection filter and show all queries\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Clear\n </button>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #clientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(clientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n <span class=\"et-query-devtools-count\">{{ filteredQueries().length }}</span>\n }\n </div>\n\n @for (item of filteredQueries(); track item.entry.id) {\n <button\n [class.et-query-devtools-row--active]=\"selectedQueryId() === item.entry.id\"\n (click)=\"selectedQueryId.set(item.entry.id)\"\n class=\"et-query-devtools-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"queryStatus(item.query)\"\n [title]=\"queryStatus(item.query)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ item.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">{{ item.entry.meta.route || '\u2014' }}</span>\n @if (isStale(item.query)) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">stale</span>\n }\n </button>\n } @empty {\n @if (inspectFilterIds()) {\n <p class=\"et-query-devtools-empty\">\n The inspected element's queries are no longer registered. Clear the filter.\n </p>\n } @else {\n <p class=\"et-query-devtools-empty\">No queries registered.</p>\n }\n }\n </div>\n\n <div class=\"et-query-devtools-detail\">\n @if (selectedQuery(); as sel) {\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Select a query to inspect it.</p>\n }\n </div>\n </div>\n }\n\n <!-- STACKS -->\n @case ('stacks') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of stackEntries(); track entry.id) {\n @if (entry.kind === 'paged-query-stack') {\n @let paged = asPagedStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">paged stack</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ paged.direction() }}</span>\n @if (paged.loading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n </div>\n\n @let pagedId = stackIdentity(paged);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ pagedId.method }}</span>\n <span class=\"et-query-devtools-route\">{{ pagedId.route }}</span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ pagedId.baseUrl }}</div>\n @if (entry.meta.featureTypes?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (feature of entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Pages loaded</dt>\n <dd>{{ paged.queries().length }}</dd>\n <dt>Items</dt>\n <dd>{{ paged.items().length }}</dd>\n <dt>First page loaded</dt>\n <dd>{{ paged.isFirstPageLoaded() }}</dd>\n <dt>Last page loaded</dt>\n <dd>{{ paged.isLastPageLoaded() }}</dd>\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(paged) }\" />\n </div>\n } @else {\n @let stack = asStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">stack</span>\n @if (stack.anyLoading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n @if (stack.anyError()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">error</span>\n }\n </div>\n\n @let stackId = stackIdentity(stack);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ stackId.method }}</span>\n <span class=\"et-query-devtools-route\">{{ stackId.route }}</span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ stackId.baseUrl }}</div>\n @if (entry.meta.featureTypes?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (feature of entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Queries</dt>\n <dd>{{ stack.queries().length }}</dd>\n <dt>Loading</dt>\n <dd>{{ stack.loadingProgress().loaded }} / {{ stack.loadingProgress().total }}</dd>\n <dt>Errors</dt>\n <dd>{{ stack.errors().length }}</dd>\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(stack) }\" />\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No stacks registered.</p>\n }\n </div>\n\n @if (stackSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"stackSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- SEQUENCES -->\n @case ('sequences') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of sequenceEntries(); track entry.id) {\n @let sequence = asSequence(entry);\n @if (sequence) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">sequence</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ sequence.status() }}</span>\n <span class=\"et-query-devtools-count\"\n >step {{ sequence.currentStep() }} / {{ sequence.total }}</span\n >\n </div>\n\n <ol class=\"et-query-devtools-step-list\">\n @for (link of queriesForSequence(sequence); track $index; let i = $index) {\n <li [attr.data-status]=\"sequenceStepStatus(sequence, i)\" class=\"et-query-devtools-step-item\">\n <div class=\"et-query-devtools-step-head\">\n <button\n [class.et-query-devtools-inner-row--active]=\"sequenceSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"sequenceSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"sequenceStepStatus(sequence, i)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ i + 1 }}. {{ link.method }}</span>\n <span class=\"et-query-devtools-route\">{{ link.route || 'step ' + (i + 1) }}</span>\n </button>\n <button\n [attr.aria-expanded]=\"isStepExpanded(entry.id, i)\"\n (click)=\"toggleStep(entry.id, i)\"\n class=\"et-query-devtools-step-caret-btn\"\n type=\"button\"\n title=\"Toggle in / out\"\n >\n {{ isStepExpanded(entry.id, i) ? '\u25BE' : '\u25B8' }}\n </button>\n </div>\n\n @if (isStepExpanded(entry.id, i)) {\n @let snap = stepSnapshot(sequence, i);\n <div class=\"et-query-devtools-step-io\">\n <h5 class=\"et-query-devtools-section\">In \u00B7 args</h5>\n @if (sequence.stepArgs().length > i) {\n <et-query-devtools-json [value]=\"sequence.stepArgs()[i]\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n\n @if (snap?.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 error ({{ error.raw.status }})</h5>\n <et-query-devtools-json [value]=\"error.isList ? error.errors : error.error\" />\n } @else {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 response</h5>\n @if (snap) {\n <et-query-devtools-json [value]=\"snap.response()\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n }\n </div>\n }\n </li>\n }\n </ol>\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No sequences registered.</p>\n }\n </div>\n\n @if (sequenceSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"sequenceSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- AUTH -->\n @case ('auth') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of authEntries(); track entry.id) {\n @let auth = asAuth(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'auth provider' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"auth.isAuthenticated()\"\n [class.et-query-devtools-chip--muted]=\"!auth.isAuthenticated()\"\n class=\"et-query-devtools-chip\"\n >\n {{ auth.isAuthenticated() ? 'authenticated' : 'anonymous' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.clientBaseUrl || entry.meta.clientName || '\u2014' }}</dd>\n <dt>Access token</dt>\n <dd>{{ auth.accessToken() ? 'present' : 'none' }}</dd>\n <dt>Refresh token</dt>\n <dd>{{ auth.refreshToken() ? 'present' : 'none' }}</dd>\n @if (authTokenExpiry(auth); as expiry) {\n <dt>Token expires in</dt>\n <dd>{{ expiry }}</dd>\n }\n @if (auth.executionState(); as state) {\n <dt>Execution state</dt>\n <dd>{{ state.type }} \u00B7 {{ state.state }}</dd>\n }\n @if (authQueryKeys(auth).length) {\n <dt>Queries</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (key of authQueryKeys(auth); track key) {\n <span class=\"et-query-devtools-chip\">{{ key }}</span>\n }\n </div>\n </dd>\n }\n @if (authFeatures(auth).length) {\n <dt>Features</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (feature of authFeatures(auth); track feature) {\n <span class=\"et-query-devtools-chip\">{{ feature }}</span>\n }\n </div>\n </dd>\n }\n </dl>\n\n @if (authTokenPayload(auth); as payload) {\n <h4 class=\"et-query-devtools-section\">Access token payload</h4>\n <et-query-devtools-json [value]=\"payload\" />\n }\n\n @if (auth.latestNonInternalQuery(); as latest) {\n <h4 class=\"et-query-devtools-section\">Latest auth query \u00B7 {{ latest.key }}</h4>\n <et-query-devtools-json [value]=\"latest.snapshot.response()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No auth providers registered.</p>\n }\n </div>\n }\n\n <!-- SOCKETS -->\n @case ('ws') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of wsEntries(); track entry.id) {\n @let ws = asWs(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'socket' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"ws.connected()\"\n [class.et-query-devtools-chip--muted]=\"!ws.connected()\"\n class=\"et-query-devtools-chip\"\n >\n {{ ws.connected() ? 'connected' : 'disconnected' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.url }}</dd>\n <dt>Rooms</dt>\n <dd>\n @if (ws.rooms().length) {\n <div class=\"et-query-devtools-chips\">\n @for (room of ws.rooms(); track room) {\n <span class=\"et-query-devtools-chip\">{{ room }}</span>\n }\n </div>\n } @else {\n none\n }\n </dd>\n </dl>\n\n <h5 class=\"et-query-devtools-section\">Messages ({{ ws.messages().length }})</h5>\n @for (message of ws.messages(); track message.id) {\n <div class=\"et-query-devtools-ws-message\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-count\">{{ formatTime(message.timestamp) }}</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ message.room }}</span>\n <span class=\"et-query-devtools-method\">{{ message.event }}</span>\n </div>\n <et-query-devtools-json [value]=\"message.data\" />\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No messages received yet.</p>\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No web socket clients registered.</p>\n }\n </div>\n }\n\n <!-- CACHE -->\n @case ('cache') {\n <div class=\"et-query-devtools-scroll\">\n @for (client of cacheView(); track client.name) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.baseUrl || client.name }}</strong>\n <span class=\"et-query-devtools-count\">{{ client.entries.length }} entries</span>\n @if (client.client?.subtle?.persistence) {\n <span class=\"et-query-devtools-count\">{{ persistedCount(client.client!) }} on disk</span>\n <button (click)=\"clearPersistedQueries(client.client!)\" class=\"et-query-devtools-btn\" type=\"button\">\n Clear disk\n </button>\n }\n </div>\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Request</th>\n <th>Consumers</th>\n <th>Secure</th>\n <th>Freshness</th>\n <th>Sync</th>\n <th>Disk</th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (cacheEntry of client.entries; track cacheEntry.key) {\n <tr>\n <td [title]=\"cacheEntry.request.method + ' ' + cacheEntry.request.url\">\n <span class=\"et-query-devtools-method\">{{ cacheEntry.request.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(cacheEntry.request.url) }}</span>\n </td>\n <td>{{ cacheEntry.consumerCount }}</td>\n <td>{{ cacheEntry.isSecure ? 'yes' : 'no' }}</td>\n <td>{{ cacheFreshness(cacheEntry) }}</td>\n <td>{{ cacheSync(cacheEntry, client.pollStates) }}</td>\n <td>{{ cachePersistence(cacheEntry) }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n <button (click)=\"refetchCacheEntry(cacheEntry)\" class=\"et-query-devtools-btn\" type=\"button\">\n Refetch\n </button>\n <button\n (click)=\"evictCacheEntry(client.repository, cacheEntry.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Evict\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"7\">Cache empty.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No clients with a cache yet.</p>\n }\n </div>\n }\n\n <!-- EVENTS -->\n @case ('events') {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-toolbar\">\n <span class=\"et-query-devtools-count\">{{ eventLog().length }} events</span>\n <button (click)=\"clearEvents()\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Base URL</th>\n <th>Type</th>\n <th>Request</th>\n </tr>\n </thead>\n <tbody>\n @for (event of eventLog(); track event.id) {\n <tr [attr.data-status]=\"event.type === 'request-error' ? 'error' : 'success'\">\n <td>{{ formatTime(event.timestamp) }}</td>\n <td class=\"et-query-devtools-mono\">{{ event.client }}</td>\n <td>\n {{ eventTypeLabel(event) }}\n @if (event.isSecure) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">secure</span>\n }\n </td>\n @if (event.url; as url) {\n <td [title]=\"event.method + ' ' + url\">\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </td>\n } @else {\n <td class=\"et-query-devtools-url\">all secure entries dropped</td>\n }\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"4\">No events captured yet.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n }\n </div>\n\n <!-- Reusable inner-query list (stacks) \u2014 clicking opens the split-view drawer, not a tab switch. -->\n <ng-template #innerQueriesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track $index) {\n <button\n [class.et-query-devtools-inner-row--active]=\"stackSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"stackSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">{{ link.route || link.query.id() || 'query ' + ($index + 1) }}</span>\n </button>\n }\n </div>\n </ng-template>\n\n <!-- Reusable query detail \u2014 used by the Queries tab and the stacks split-view drawer. -->\n <ng-template #queryDetailTpl let-sel=\"sel\">\n <div class=\"et-query-devtools-detail-head\">\n <span class=\"et-query-devtools-method\">{{ sel.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">{{ sel.entry.meta.route || '\u2014' }}</span>\n </div>\n\n <div class=\"et-query-devtools-actions\">\n <span class=\"et-query-devtools-actions-label\">Run</span>\n <button (click)=\"executeQuery(sel.query, false)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"executeQuery(sel.query, true)\" class=\"et-query-devtools-btn\" type=\"button\">Cached</button>\n <button (click)=\"resetQuery(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Reset</button>\n <button (click)=\"copyReport(sel.entry, sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">\n {{ copiedReport() ? '\u2713 Copied' : 'Copy report' }}\n </button>\n <span class=\"et-query-devtools-actions-label\">Edit</span>\n <button (click)=\"openResponseEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Response</button>\n <button (click)=\"openArgsEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Replay args</button>\n <span class=\"et-query-devtools-actions-label\">Force</span>\n <button (click)=\"forceLoading(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Loading</button>\n <button (click)=\"forceError(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Error</button>\n <button (click)=\"forceEmpty(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Empty</button>\n <button (click)=\"clearForced(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n\n @if (editorMode() !== 'none') {\n <div class=\"et-query-devtools-editor\">\n @if (editorMode() === 'response') {\n <h4 class=\"et-query-devtools-section\">Edit response \u2192 setResponse()</h4>\n <textarea\n #respArea\n [value]=\"responseDraft()\"\n (input)=\"responseDraft.set(respArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyResponse(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Apply</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n } @else {\n <h4 class=\"et-query-devtools-section\">Replay with edited args \u2192 execute()</h4>\n <textarea\n #argsArea\n [value]=\"argsDraft()\"\n (input)=\"argsDraft.set(argsArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyArgs(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n }\n @if (editError(); as err) {\n <p class=\"et-query-devtools-editor-error\">{{ err }}</p>\n }\n </div>\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.entry.meta.clientBaseUrl || sel.entry.meta.clientName || '\u2014' }}</dd>\n <dt>Status</dt>\n <dd>{{ queryStatus(sel.query) }}</dd>\n <dt>Cache key</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.query.id() || '\u2014' }}</dd>\n <dt>Last executed</dt>\n <dd>{{ formatTime(sel.query.lastTimeExecutedAt()) }}</dd>\n <dt>Triggered by</dt>\n <dd>{{ sel.query.triggeredBy() || 'user' }}</dd>\n @if (sel.entry.meta.featureTypes?.length) {\n <dt>Features</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (feature of sel.entry.meta.featureTypes; track feature) {\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature) }}</span>\n }\n </div>\n </dd>\n }\n </dl>\n\n @if (sel.entry.meta.gqlQuery; as gqlQuery) {\n <h5 class=\"et-query-devtools-section\">GraphQL document</h5>\n <pre class=\"et-query-devtools-code\">{{ gqlDocument(gqlQuery) }}</pre>\n }\n\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Value explorer</h4>\n <input\n #jsonSearchInput\n [value]=\"jsonSearch()\"\n (input)=\"jsonSearch.set(jsonSearchInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter keys / values\u2026\"\n />\n </div>\n\n <h5 class=\"et-query-devtools-section\">Args</h5>\n <et-query-devtools-json\n [value]=\"sel.query.args()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"args\"\n />\n\n @if (sel.query.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Error ({{ error.raw.status }})</h5>\n <et-query-devtools-json\n [value]=\"error.isList ? error.errors : error.error\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"error\"\n />\n } @else {\n <h5 class=\"et-query-devtools-section\">Response</h5>\n <et-query-devtools-json\n [value]=\"sel.query.response()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"response\"\n />\n }\n </ng-template>\n </section>\n}\n\n@if (!open()) {\n <et-query-devtools-toggle (openChange)=\"toggleOpen()\" />\n}\n\n@if (inspectActive() && inspectHover(); as hover) {\n <div\n [style.left.px]=\"hover.rect.left\"\n [style.top.px]=\"hover.rect.top\"\n [style.width.px]=\"hover.rect.width\"\n [style.height.px]=\"hover.rect.height\"\n class=\"et-query-devtools-inspect-box\"\n >\n <span class=\"et-query-devtools-inspect-tag\">{{ inspectLabel(hover.entries) }}</span>\n </div>\n}\n", styles: ["@layer components{.et-query-devtools-host{--_et-qdt-bg: var(--et-surface-background-solid, rgb(24 24 27));--_et-qdt-fg: var(--et-surface-color-solid, rgb(244 244 245));--_et-qdt-muted: var(--et-surface-color-muted-solid, rgb(161 161 170));--_et-qdt-subtle: var(--et-surface-color-subtle-solid, rgb(113 113 122));--_et-qdt-border: var(--et-surface-border-solid, rgb(255 255 255 / .12));--_et-qdt-interaction: var(--et-surface-interaction-solid, rgb(255 255 255));--_et-qdt-accent: var(--et-theme-color-primary-solid, rgb(96 165 250));--_et-qdt-hover: color-mix(in srgb, var(--_et-qdt-interaction) 10%, transparent);--_et-qdt-active: color-mix(in srgb, var(--_et-qdt-interaction) 18%, transparent);--_et-qdt-success: rgb(34 197 94);--_et-qdt-error: rgb(239 68 68);--_et-qdt-loading: rgb(234 179 8);position:fixed;inset-inline:0;inset-block-end:0;z-index:2147483000;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,Liberation Mono,monospace;font-size:1.2rem;line-height:1.4;pointer-events:none}.et-query-devtools-host>*{pointer-events:auto}.et-query-devtools-panel{position:relative;display:flex;flex-direction:column;inline-size:100%;min-block-size:20rem;max-block-size:90vh;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border-block-start:1px solid var(--_et-qdt-border);box-shadow:0 -.4rem 2rem #00000059}.et-query-devtools-panel--resizing{-webkit-user-select:none;user-select:none}.et-query-devtools-resize{position:absolute;inset-inline:0;inset-block-start:0;block-size:.6rem;cursor:ns-resize;z-index:1}.et-query-devtools-resize:after{content:\"\";position:absolute;inset-inline:50%;inset-block-start:.2rem;inline-size:4rem;margin-inline-start:-2rem;block-size:.2rem;border-radius:999px;background:var(--_et-qdt-border)}.et-query-devtools-resize:hover:after{background:var(--_et-qdt-accent)}.et-query-devtools-header{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding-inline:.8rem;border-block-end:1px solid var(--_et-qdt-border);flex:0 0 auto}.et-query-devtools-tabs{display:flex;flex-wrap:wrap;gap:.2rem}.et-query-devtools-tab{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);padding:.9rem 1rem;cursor:pointer;border-block-end:2px solid transparent;font:inherit}.et-query-devtools-tab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-tab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-header-actions{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}.et-query-devtools-inspect-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.4rem .8rem;font:inherit}.et-query-devtools-inspect-btn:hover{color:var(--_et-qdt-fg)}.et-query-devtools-inspect-btn--active{color:#09090b;background:var(--_et-qdt-accent);border-color:var(--_et-qdt-accent)}.et-query-devtools-close{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.6rem;font:inherit}.et-query-devtools-close-panel{appearance:none;display:inline-flex;align-items:center;gap:.6rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:var(--_et-qdt-hover);color:var(--_et-qdt-fg);cursor:pointer;padding:.4rem .6rem .4rem .8rem;font:inherit}.et-query-devtools-close-panel:hover{border-color:var(--_et-qdt-error);color:var(--_et-qdt-error)}.et-query-devtools-kbd{border-radius:.3rem;background:var(--_et-qdt-hover);box-shadow:inset 0 0 0 1px var(--_et-qdt-border);color:var(--_et-qdt-muted);padding:.15rem .4rem;font:inherit;font-size:1.1rem;line-height:1.2;white-space:nowrap}.et-query-devtools-inspect-box{position:fixed;z-index:2147483002;pointer-events:none;border:2px solid var(--_et-qdt-accent);background:color-mix(in srgb,var(--_et-qdt-accent) 15%,transparent);border-radius:.2rem}.et-query-devtools-inspect-tag{position:absolute;inset-block-start:100%;inset-inline-start:0;margin-block-start:.2rem;padding:.2rem .5rem;border-radius:.3rem;background:var(--_et-qdt-accent);color:#09090b;font-family:ui-monospace,monospace;font-size:1.1rem;font-weight:600;white-space:nowrap}.et-query-devtools-close:hover{color:var(--_et-qdt-fg)}.et-query-devtools-body{flex:1 1 auto;min-block-size:0;overflow:hidden}.et-query-devtools-master-detail{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-list{flex:0 0 34%;min-inline-size:22rem;max-inline-size:40rem;overflow-y:auto;border-inline-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail{flex:1 1 auto;min-inline-size:0;overflow-y:auto;padding:1rem}.et-query-devtools-detail .et-query-devtools-detail-head,.et-query-devtools-drawer .et-query-devtools-detail-head{padding-block-end:.8rem;border-block-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail .et-query-devtools-meta,.et-query-devtools-drawer .et-query-devtools-meta{padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-detail .et-query-devtools-section-row,.et-query-devtools-drawer .et-query-devtools-section-row{padding-block-start:.8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-filter-chip{display:inline-flex;align-items:center;gap:.6rem;padding:.3rem .35rem .3rem .7rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-accent) 16%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_et-qdt-accent) 45%,transparent);color:var(--_et-qdt-accent);font-size:1.1rem;font-weight:600;letter-spacing:.02em}.et-query-devtools-filter-icon{font-size:1.3rem;line-height:1}.et-query-devtools-filter-count{border-radius:999px;background:var(--_et-qdt-accent);color:#09090b;min-inline-size:1.8rem;padding:0 .5rem;text-align:center}.et-query-devtools-filter-clear{appearance:none;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.3rem .6rem;font:inherit;font-size:1.15rem;line-height:1.4}.et-query-devtools-filter-clear:hover{border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-scroll{block-size:100%;overflow:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.et-query-devtools-split{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-split .et-query-devtools-scroll{flex:1 1 auto;min-inline-size:0}.et-query-devtools-drawer{flex:0 0 42%;min-inline-size:26rem;max-inline-size:48rem;overflow-y:auto;padding:0 1rem 1rem;border-inline-start:1px solid var(--_et-qdt-border)}.et-query-devtools-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem 0;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg);color:var(--_et-qdt-muted);border-block-end:1px solid var(--_et-qdt-border);margin-block-end:.8rem}.et-query-devtools-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-toolbar--filtered{background:linear-gradient(to right,color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent),transparent 65%),var(--_et-qdt-bg);box-shadow:inset .2rem 0 0 var(--_et-qdt-accent),inset 0 -1px color-mix(in srgb,var(--_et-qdt-accent) 30%,transparent)}.et-query-devtools-field{display:inline-flex;align-items:center;gap:.6rem;color:var(--_et-qdt-muted)}.et-query-devtools-select{background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-count{color:var(--_et-qdt-subtle)}.et-query-devtools-row{display:flex;align-items:center;gap:.8rem;inline-size:100%;text-align:start;appearance:none;border:none;border-block-end:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-fg);padding:.8rem;cursor:pointer;font:inherit}.et-query-devtools-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-row--active{background:var(--_et-qdt-active)}.et-query-devtools-method{font-weight:700;font-size:1.1rem;color:var(--_et-qdt-muted);white-space:nowrap}.et-query-devtools-route{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-inline-size:0}.et-query-devtools-status{inline-size:.8rem;block-size:.8rem;border-radius:999px;flex:0 0 auto;background:var(--_et-qdt-subtle)}.et-query-devtools-status[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-status[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-status[data-status=loading],.et-query-devtools-status[data-status=running]{background:var(--_et-qdt-loading)}.et-query-devtools-detail-head{display:flex;align-items:center;gap:.8rem;margin-block-end:.8rem}.et-query-devtools-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-block-end:.8rem}.et-query-devtools-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.3rem;background:transparent;color:var(--_et-qdt-fg);padding:.25rem .55rem;font-size:1.15rem;line-height:1.4;cursor:pointer}.et-query-devtools-btn:hover{background:var(--_et-qdt-hover)}.et-query-devtools-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;margin:0 0 1rem}.et-query-devtools-meta dt{color:var(--_et-qdt-muted)}.et-query-devtools-meta dd{margin:0;overflow-wrap:anywhere}.et-query-devtools-section{margin:1rem 0 .4rem;font-size:1.1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-json{margin:0;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere;max-block-size:24rem;overflow-y:auto}.et-query-devtools-json--error{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-mono{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-code{margin:0 0 .8rem;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1.15rem;max-block-size:20rem}.et-query-devtools-ws-message{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;margin-block-end:.4rem}.et-query-devtools-url{display:inline-block;max-inline-size:32rem;margin-inline-start:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.et-query-devtools-chips,.et-query-devtools-chip-row{display:flex;flex-wrap:wrap;gap:.4rem;margin-block-end:.8rem}.et-query-devtools-chip{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .6rem;border-radius:999px;border:1px solid var(--_et-qdt-border);font-size:1.1rem;color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-interaction) 6%,transparent)}.et-query-devtools-chip--muted{color:var(--_et-qdt-muted)}.et-query-devtools-chip--loading{color:var(--_et-qdt-loading);border-color:color-mix(in srgb,var(--_et-qdt-loading) 50%,transparent)}.et-query-devtools-chip--error{color:var(--_et-qdt-error);border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-chip--success{color:var(--_et-qdt-success);border-color:color-mix(in srgb,var(--_et-qdt-success) 50%,transparent)}.et-query-devtools-card{border:1px solid var(--_et-qdt-border);border-radius:.6rem;padding:1rem}.et-query-devtools-card-head{display:flex;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-card-title{display:flex;align-items:baseline;gap:.5rem;margin-block-end:.2rem}.et-query-devtools-card-sub{color:var(--_et-qdt-subtle);font-size:1.1rem;margin-block-end:.6rem;overflow-wrap:anywhere}.et-query-devtools-inner-queries{display:flex;flex-direction:column;gap:.3rem}.et-query-devtools-inner-row{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-fg);padding:.4rem .6rem;cursor:pointer;font:inherit}.et-query-devtools-inner-row:hover:not([disabled]){background:var(--_et-qdt-hover)}.et-query-devtools-inner-row--active{background:var(--_et-qdt-active);border-color:var(--_et-qdt-accent)}.et-query-devtools-inner-row[disabled]{cursor:default;opacity:.6}.et-query-devtools-step-list{display:flex;flex-direction:column;gap:.4rem;margin:0;padding:0;list-style:none}.et-query-devtools-step-item{border:1px solid var(--_et-qdt-border);border-radius:.4rem;overflow:hidden}.et-query-devtools-step-item[data-status=error]{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-step-head{display:flex;align-items:stretch}.et-query-devtools-step-head .et-query-devtools-inner-row{border:none;border-radius:0;flex:1 1 auto}.et-query-devtools-step-caret-btn{appearance:none;border:none;border-inline-start:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:0 .7rem;font:inherit}.et-query-devtools-step-caret-btn:hover{background:var(--_et-qdt-hover);color:var(--_et-qdt-fg)}.et-query-devtools-step-toggle{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:none;background:transparent;color:var(--_et-qdt-fg);padding:.6rem;cursor:pointer;font:inherit}.et-query-devtools-step-toggle:hover{background:var(--_et-qdt-hover)}.et-query-devtools-step-index{flex:1 1 auto;min-inline-size:0}.et-query-devtools-step-caret{color:var(--_et-qdt-muted)}.et-query-devtools-step-io{padding:0 .8rem .8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-table{inline-size:100%;border-collapse:collapse}.et-query-devtools-table th,.et-query-devtools-table td{text-align:start;padding:.5rem .6rem;border-block-end:1px solid var(--_et-qdt-border);vertical-align:top}.et-query-devtools-table th{color:var(--_et-qdt-muted);font-weight:600;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-table tr[data-status=error] td{color:var(--_et-qdt-error)}.et-query-devtools-empty{color:var(--_et-qdt-subtle);padding:1.2rem;text-align:center}.et-query-devtools-actions-label{color:var(--_et-qdt-subtle);align-self:center;text-transform:uppercase;font-size:1rem;letter-spacing:.05em}.et-query-devtools-actions-label:not(:first-child){margin-inline-start:.7rem}.et-query-devtools-cell-actions{display:flex;gap:.4rem;white-space:nowrap}.et-query-devtools-section-row{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-block-start:1rem}.et-query-devtools-search{flex:0 1 20rem;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-editor{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.8rem;margin-block-end:1rem}.et-query-devtools-textarea{inline-size:100%;min-block-size:12rem;resize:vertical;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;font-family:inherit;font-size:1.2rem}.et-query-devtools-editor-error{color:var(--_et-qdt-error);margin:.4rem 0 0}.et-query-devtools-json-node,.et-query-devtools-json-children{display:flex;flex-direction:column}.et-query-devtools-json-children{padding-inline-start:1.4rem;border-inline-start:1px solid var(--_et-qdt-border);margin-inline-start:.4rem}.et-query-devtools-json-row{display:flex;align-items:baseline;gap:.4rem;padding:.1rem 0;font-family:inherit;line-height:1.5}.et-query-devtools-json-toggle{appearance:none;display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;border:none;background:transparent;color:var(--_et-qdt-fg);cursor:pointer;text-align:start;font:inherit;padding:.1rem 0}.et-query-devtools-json-node>.et-query-devtools-json-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-json-caret{color:var(--_et-qdt-subtle);inline-size:1rem;flex:0 0 auto}.et-query-devtools-json-key{color:var(--_et-qdt-accent)}.et-query-devtools-json-colon,.et-query-devtools-json-preview{color:var(--_et-qdt-subtle)}.et-query-devtools-json-value{overflow-wrap:anywhere}.et-query-devtools-json-leaf[data-kind=string] .et-query-devtools-json-value{color:var(--_et-qdt-success)}.et-query-devtools-json-leaf[data-kind=number] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=boolean] .et-query-devtools-json-value{color:var(--_et-qdt-loading)}.et-query-devtools-json-leaf[data-kind=null] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=undefined] .et-query-devtools-json-value{color:var(--_et-qdt-subtle)}.et-query-devtools-json-hit{background:color-mix(in srgb,var(--_et-qdt-loading) 40%,transparent);border-radius:.2rem}.et-query-devtools-json-copy{appearance:none;flex:0 0 auto;border:none;background:transparent;color:var(--_et-qdt-subtle);cursor:pointer;opacity:0;font:inherit}.et-query-devtools-json-copy:hover:not(.et-query-devtools-json-copy--copied){color:var(--_et-qdt-fg)}.et-query-devtools-json-row:hover .et-query-devtools-json-copy,.et-query-devtools-json-copy:focus-visible,.et-query-devtools-json-copy--copied{opacity:1}.et-query-devtools-json-copy--copied{color:var(--_et-qdt-success)}}\n"] }]
|
|
49336
|
+
}, template: "@if (open()) {\n <section\n [class.et-query-devtools-panel--resizing]=\"resizing()\"\n [style.height.px]=\"panelHeight()\"\n class=\"et-query-devtools-panel\"\n >\n <div\n (pointerdown)=\"startResize($event)\"\n class=\"et-query-devtools-resize\"\n title=\"Drag to resize\"\n aria-hidden=\"true\"\n ></div>\n <header class=\"et-query-devtools-header\">\n <nav class=\"et-query-devtools-tabs\" role=\"tablist\">\n @for (tab of tabs; track tab.id) {\n @let badge = tabBadges()[tab.id];\n <button\n [class.et-query-devtools-tab--active]=\"activeTab() === tab.id\"\n [attr.aria-selected]=\"activeTab() === tab.id\"\n (click)=\"activeTab.set(tab.id)\"\n class=\"et-query-devtools-tab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n @if (badge.count) {\n <span class=\"et-query-devtools-tab-badge\">{{ badge.count }}</span>\n }\n @if (badge.errors) {\n <span\n class=\"et-query-devtools-tab-badge et-query-devtools-tab-badge--error\"\n title=\"{{ badge.errors }} {{ badge.errorNoun ?? 'failing' }}\"\n >\n {{ badge.errors }}\n </span>\n }\n </button>\n }\n </nav>\n\n <div class=\"et-query-devtools-header-actions\">\n <button\n [class.et-query-devtools-inspect-btn--active]=\"inspectActive()\"\n [attr.aria-pressed]=\"inspectActive()\"\n (click)=\"toggleInspect()\"\n class=\"et-query-devtools-inspect-btn\"\n type=\"button\"\n title=\"Inspect: hover the UI to find its query\"\n >\n <svg\n class=\"et-query-devtools-inspect-icon\"\n viewBox=\"0 0 16 16\"\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <circle class=\"et-query-devtools-inspect-icon-ring\" cx=\"8\" cy=\"8\" r=\"4.25\" />\n <circle class=\"et-query-devtools-inspect-icon-dot\" cx=\"8\" cy=\"8\" r=\"1.15\" />\n <path d=\"M8 0.9v2.1M8 13v2.1M0.9 8h2.1M13 8h2.1\" />\n </svg>\n {{ inspectActive() ? 'Inspecting' : 'Inspect' }}\n @if (inspectActive()) {\n <kbd class=\"et-query-devtools-kbd et-query-devtools-kbd--inverse\">Esc</kbd>\n }\n </button>\n <button\n (click)=\"toggleOpen()\"\n class=\"et-query-devtools-close-panel\"\n type=\"button\"\n title=\"Close devtools ({{ shortcut }})\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Close\n <kbd class=\"et-query-devtools-kbd\">{{ shortcut }}</kbd>\n </button>\n </div>\n </header>\n\n @if (armedFaultClients(); as armed) {\n <div class=\"et-query-devtools-armed-bar\" role=\"status\">\n <span class=\"et-query-devtools-armed-dot\" aria-hidden=\"true\"></span>\n <span class=\"et-query-devtools-armed-text\">\n <strong>Faults armed</strong> on\n <span class=\"et-query-devtools-mono\">{{ armed.join(', ') }}</span>\n \u2014 every failure and delay you see from\n {{ armed.length === 1 ? 'this client' : 'these clients' }} is injected, not the API.\n </span>\n <button\n [disabled]=\"activeTab() === 'faults'\"\n (click)=\"activeTab.set('faults')\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Review\n </button>\n <button (click)=\"CLEAR_FAULTS()\" class=\"et-query-devtools-btn\" type=\"button\">Disarm all</button>\n </div>\n }\n\n <div class=\"et-query-devtools-body\">\n @switch (activeTab()) {\n <!-- QUERIES -->\n @case ('queries') {\n <div class=\"et-query-devtools-master-detail\">\n <div class=\"et-query-devtools-list\">\n <div [class.et-query-devtools-toolbar--filtered]=\"!!inspectFilterIds()\" class=\"et-query-devtools-toolbar\">\n <div class=\"et-query-devtools-toolbar-row\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n <button\n (click)=\"clearInspectFilter()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the inspection filter and show all queries\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n Clear\n </button>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #clientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(clientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n }\n <span class=\"et-query-devtools-count\">\n @if (isQueryListNarrowed()) {\n {{ filteredQueries().length }} of {{ scopedQueryCount() }}\n } @else {\n {{ scopedQueryCount() }}\n }\n </span>\n <button\n [disabled]=\"!filteredQueries().length\"\n (click)=\"downloadInsomniaCollection()\"\n class=\"et-query-devtools-btn et-query-devtools-toolbar-btn\"\n type=\"button\"\n title=\"Download the listed queries as an Insomnia collection, with the auth provider's token refresh\"\n >\n \u2913 Insomnia\n </button>\n </div>\n\n <div class=\"et-query-devtools-toolbar-row\">\n <input\n #queryFilterInput\n [value]=\"queryFilter()\"\n (input)=\"queryFilter.set(queryFilterInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter method / route / url\u2026\"\n aria-label=\"Filter queries\"\n />\n @for (facet of facets; track facet.id) {\n @let count = facetCounts()[facet.id];\n @let isActive = queryFacets().has(facet.id);\n <button\n [class.et-query-devtools-facet--active]=\"isActive\"\n [attr.aria-pressed]=\"isActive\"\n [disabled]=\"!count && !isActive\"\n (click)=\"toggleFacet(facet.id)\"\n class=\"et-query-devtools-chip et-query-devtools-facet\"\n type=\"button\"\n >\n {{ facet.label }}\n <span class=\"et-query-devtools-facet-count\">{{ count }}</span>\n </button>\n }\n @if (isQueryListNarrowed()) {\n <button\n (click)=\"clearQueryFilters()\"\n class=\"et-query-devtools-filter-clear\"\n type=\"button\"\n title=\"Clear the search term and the status chips\"\n aria-label=\"Clear filters\"\n >\n <span aria-hidden=\"true\">\u2715</span>\n </button>\n }\n </div>\n </div>\n\n @for (item of filteredQueries(); track item.entry.id) {\n <button\n [class.et-query-devtools-row--active]=\"selectedQueryId() === item.entry.id\"\n (click)=\"selectedQueryId.set(item.entry.id)\"\n class=\"et-query-devtools-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"queryStatus(item.query)\"\n [title]=\"queryStatus(item.query)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ item.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: routeSegments(item.entry, item.query), fallback: '\u2014' }\n \"\n />\n </span>\n <!-- A retry is the one thing a loading dot actively misrepresents: it reads as a request\n on its way when nothing is in flight at all. -->\n @if (requestProgress(item.query)?.retry; as retry) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Waiting out a retry backoff before attempt {{ retry.attempt }}\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ retry.attempt }}\n </span>\n }\n @if (isStale(item.query)) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">stale</span>\n }\n </button>\n } @empty {\n <!-- An empty scope is a different problem from a filter that matches nothing, and only the\n latter is the user's own doing - so it names the way out. -->\n @if (scopedQueryCount()) {\n <p class=\"et-query-devtools-empty\">\n No query matches the filter \u00B7 {{ scopedQueryCount() }} in scope.\n </p>\n } @else if (inspectFilterIds()) {\n <p class=\"et-query-devtools-empty\">\n The inspected element's queries are no longer registered. Clear the filter.\n </p>\n } @else {\n <p class=\"et-query-devtools-empty\">No queries registered.</p>\n }\n }\n </div>\n\n <div class=\"et-query-devtools-detail\">\n @if (selectedQuery(); as sel) {\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Select a query to inspect it.</p>\n }\n </div>\n </div>\n }\n\n <!-- STACKS -->\n @case ('stacks') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of stackEntries(); track entry.id) {\n @if (entry.kind === 'paged-query-stack') {\n @let paged = asPagedStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">paged stack</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ paged.direction() }}</span>\n @if (paged.loading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n </div>\n\n @let pagedId = stackIdentity(paged);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ pagedId.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: pagedId.segments, fallback: '\u2014' }\"\n />\n </span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ pagedId.baseUrl }}</div>\n @if (entry.meta.features?.length) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Pages loaded</dt>\n <dd>{{ paged.queries().length }}</dd>\n <dt>Items</dt>\n <dd>{{ paged.items().length }}</dd>\n <dt>First page loaded</dt>\n <dd>{{ paged.isFirstPageLoaded() }}</dd>\n <dt>Last page loaded</dt>\n <dd>{{ paged.isLastPageLoaded() }}</dd>\n <ng-container *ngTemplateOutlet=\"activityRowTpl; context: { activity: stackActivity(paged) }\" />\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(paged) }\" />\n </div>\n } @else {\n @let stack = asStack(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">stack</span>\n @if (stack.anyLoading()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">loading</span>\n }\n @if (stack.anyError()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">error</span>\n }\n </div>\n\n @let stackId = stackIdentity(stack);\n <div class=\"et-query-devtools-card-title\">\n <span class=\"et-query-devtools-method\">{{ stackId.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: stackId.segments, fallback: '\u2014' }\"\n />\n </span>\n </div>\n <div class=\"et-query-devtools-card-sub et-query-devtools-mono\">{{ stackId.baseUrl }}</div>\n @if (entry.meta.features?.length) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n }\n\n <dl class=\"et-query-devtools-meta\">\n <dt>Queries</dt>\n <dd>{{ stack.queries().length }}</dd>\n <dt>Loading</dt>\n <dd>{{ stack.loadingProgress().loaded }} / {{ stack.loadingProgress().total }}</dd>\n <dt>Errors</dt>\n <dd>{{ stack.errors().length }}</dd>\n <ng-container *ngTemplateOutlet=\"activityRowTpl; context: { activity: stackActivity(stack) }\" />\n </dl>\n <ng-container *ngTemplateOutlet=\"innerQueriesTpl; context: { links: queriesForStack(stack) }\" />\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No stacks registered.</p>\n }\n </div>\n\n @if (stackSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"stackSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- SEQUENCES -->\n @case ('sequences') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of sequenceEntries(); track entry.id) {\n @let sequence = asSequence(entry);\n @if (sequence) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-chip\">sequence</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ sequence.status() }}</span>\n <span class=\"et-query-devtools-count\"\n >step {{ sequence.currentStep() }} / {{ sequence.total }}</span\n >\n </div>\n\n <dl class=\"et-query-devtools-meta\">\n <ng-container\n *ngTemplateOutlet=\"activityRowTpl; context: { activity: sequenceActivity(sequence) }\"\n />\n </dl>\n\n <ol class=\"et-query-devtools-step-list\">\n @for (link of queriesForSequence(sequence); track $index; let i = $index) {\n <li [attr.data-status]=\"sequenceStepStatus(sequence, i)\" class=\"et-query-devtools-step-item\">\n <div class=\"et-query-devtools-step-head\">\n <button\n [class.et-query-devtools-inner-row--active]=\"sequenceSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"sequenceSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span\n [attr.data-status]=\"sequenceStepStatus(sequence, i)\"\n class=\"et-query-devtools-status\"\n ></span>\n <span class=\"et-query-devtools-method\">{{ i + 1 }}. {{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: link.segments, fallback: 'step ' + (i + 1) }\n \"\n />\n </span>\n </button>\n <button\n [attr.aria-expanded]=\"isStepExpanded(entry.id, i)\"\n (click)=\"toggleStep(entry.id, i)\"\n class=\"et-query-devtools-step-caret-btn\"\n type=\"button\"\n title=\"Toggle in / out\"\n >\n {{ isStepExpanded(entry.id, i) ? '\u25BE' : '\u25B8' }}\n </button>\n </div>\n\n @if (isStepExpanded(entry.id, i)) {\n @let snap = stepSnapshot(sequence, i);\n <div class=\"et-query-devtools-step-io\">\n <h5 class=\"et-query-devtools-section\">In \u00B7 args</h5>\n @if (sequence.stepArgs().length > i) {\n <et-query-devtools-json [value]=\"sequence.stepArgs()[i]\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n\n @if (snap?.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 error ({{ error.raw.status }})</h5>\n <et-query-devtools-json [value]=\"error.isList ? error.errors : error.error\" />\n } @else {\n <h5 class=\"et-query-devtools-section\">Out \u00B7 response</h5>\n @if (snap) {\n <et-query-devtools-json [value]=\"snap.response()\" />\n } @else {\n <p class=\"et-query-devtools-empty\">Not run yet.</p>\n }\n }\n </div>\n }\n </li>\n }\n </ol>\n </div>\n }\n } @empty {\n <p class=\"et-query-devtools-empty\">No sequences registered.</p>\n }\n </div>\n\n @if (sequenceSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"sequenceSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- FORMS -->\n @case ('forms') {\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-scroll\">\n @for (entry of formEntries(); track entry.id) {\n @let form = asForm(entry);\n @let expanded = selectedFormId() === entry.id;\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <button\n [attr.aria-expanded]=\"expanded\"\n (click)=\"selectedFormId.set(expanded ? null : entry.id)\"\n class=\"et-query-devtools-row-link\"\n type=\"button\"\n >\n <span class=\"et-query-devtools-caret\">{{ expanded ? '\u25BE' : '\u25B8' }}</span>\n <strong>{{ entry.meta.name || 'query form' }}</strong>\n </button>\n @if (form.activeFilterCount(); as count) {\n <span class=\"et-query-devtools-chip\">{{ count }} active</span>\n } @else {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">at defaults</span>\n }\n @if (form.isCommitPending()) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--loading\">debouncing</span>\n }\n @if (!form.isObserving()) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"observe() has not been called, so nothing syncs with the URL\"\n >\n not observing\n </span>\n }\n <button\n [disabled]=\"form.isAtDefaults()\"\n (click)=\"form.resetAll()\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n \u27F2 Reset all\n </button>\n </div>\n\n @let driven = queriesDrivenByForm(entry);\n <dl class=\"et-query-devtools-meta\">\n <dt>Drives</dt>\n <dd>\n @if (driven.length) {\n <ng-container *ngTemplateOutlet=\"formDrivesTpl; context: { links: driven }\" />\n } @else {\n <span class=\"et-query-devtools-hint\">\n No query has read this form's value yet. A query picks it up the first time its\n <code>withArgs</code> reads <code>value()</code>.\n </span>\n }\n </dd>\n </dl>\n\n @if (expanded) {\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Field</th>\n <th>Value</th>\n <th>Default</th>\n <th>URL param</th>\n <th>Rules</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n @for (field of form.fields(); track field.key) {\n <tr [attr.data-status]=\"field.isDefault ? null : 'success'\">\n <td class=\"et-query-devtools-mono\">{{ field.key }}</td>\n <td class=\"et-query-devtools-mono\">\n {{ inlineValue(field.value) }}\n <!-- The live value only differs while a debounce is pending, which is exactly\n when \"the input says X but the query asked for Y\" needs explaining. -->\n @if (field.liveValue !== field.value) {\n <span class=\"et-query-devtools-stat-sub\"\n >typing {{ inlineValue(field.liveValue) }}</span\n >\n }\n </td>\n <td class=\"et-query-devtools-mono\">{{ inlineValue(field.defaultValue) }}</td>\n <td class=\"et-query-devtools-mono\">\n @if (field.queryParam === undefined) {\n <span class=\"et-query-devtools-hint\">\u2014</span>\n } @else {\n {{ field.paramKey }}={{ inlineValue(field.queryParam) }}\n }\n </td>\n <td>\n <div class=\"et-query-devtools-chips\">\n @if (field.debounceMs !== null) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">\n {{ field.debounceMs }}ms\n </span>\n }\n @if (field.isResetBy.length) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Reset to its default whenever one of these changes\"\n >\n reset by {{ field.isResetBy.join(', ') }}\n </span>\n }\n @if (!field.countsAsFilter) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Not counted by activeFilterCount\"\n >\n not a filter\n </span>\n }\n </div>\n </td>\n <td>\n <button\n [disabled]=\"field.isDefault\"\n (click)=\"form.resetField(field.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n \u27F2\n </button>\n </td>\n </tr>\n }\n </tbody>\n </table>\n\n <h4 class=\"et-query-devtools-section\">Committed value</h4>\n <et-query-devtools-json [value]=\"form.value()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">\n No query forms registered. A form created with <code>createQueryForm()</code> registers itself.\n </p>\n }\n </div>\n\n @if (formSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"formSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- AUTH -->\n @case ('auth') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of authEntries(); track entry.id) {\n @let auth = asAuth(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'auth provider' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"auth.isAuthenticated()\"\n [class.et-query-devtools-chip--muted]=\"!auth.isAuthenticated()\"\n class=\"et-query-devtools-chip\"\n >\n {{ auth.isAuthenticated() ? 'authenticated' : 'anonymous' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.clientBaseUrl || entry.meta.clientName || '\u2014' }}</dd>\n <dt>Access token</dt>\n <dd>{{ auth.accessToken() ? 'present' : 'none' }}</dd>\n <dt>Refresh token</dt>\n <dd>{{ auth.refreshToken() ? 'present' : 'none' }}</dd>\n @if (authTokenExpiry(auth); as expiry) {\n <dt>Token expires in</dt>\n <dd>{{ expiry }}</dd>\n }\n @if (auth.executionState(); as state) {\n <dt>Execution state</dt>\n <dd>{{ state.type }} \u00B7 {{ state.state }}</dd>\n }\n @if (authQueryKeys(auth).length) {\n <dt>Queries</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (key of authQueryKeys(auth); track key) {\n <span class=\"et-query-devtools-chip\">{{ key }}</span>\n }\n </div>\n </dd>\n }\n @if (entry.meta.features?.length) {\n <dt>Features</dt>\n <dd>\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: entry.meta.features }\" />\n </dd>\n }\n </dl>\n\n @if (authTokenPayload(auth); as payload) {\n <h4 class=\"et-query-devtools-section\">Access token payload</h4>\n <et-query-devtools-json [value]=\"payload\" />\n }\n\n @if (auth.latestNonInternalQuery(); as latest) {\n <h4 class=\"et-query-devtools-section\">Latest auth query \u00B7 {{ latest.key }}</h4>\n <et-query-devtools-json [value]=\"latest.snapshot.response()\" />\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No auth providers registered.</p>\n }\n </div>\n }\n\n <!-- SOCKETS -->\n @case ('ws') {\n <div class=\"et-query-devtools-scroll\">\n @for (entry of wsEntries(); track entry.id) {\n @let ws = asWs(entry);\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong>{{ entry.meta.name || 'socket' }}</strong>\n <span\n [class.et-query-devtools-chip--success]=\"ws.connected()\"\n [class.et-query-devtools-chip--muted]=\"!ws.connected()\"\n class=\"et-query-devtools-chip\"\n >\n {{ ws.connected() ? 'connected' : 'disconnected' }}\n </span>\n </div>\n <dl class=\"et-query-devtools-meta\">\n <dt>URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ entry.meta.url }}</dd>\n <dt>Rooms</dt>\n <dd>\n @if (ws.rooms().length) {\n <div class=\"et-query-devtools-chips\">\n @for (room of ws.rooms(); track room) {\n <span class=\"et-query-devtools-chip\">{{ room }}</span>\n }\n </div>\n } @else {\n none\n }\n </dd>\n </dl>\n\n <h5 class=\"et-query-devtools-section\">Messages ({{ ws.messages().length }})</h5>\n @for (message of ws.messages(); track message.id) {\n <div class=\"et-query-devtools-ws-message\">\n <div class=\"et-query-devtools-card-head\">\n <span class=\"et-query-devtools-count\">{{ formatTime(message.timestamp) }}</span>\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">{{ message.room }}</span>\n <span class=\"et-query-devtools-method\">{{ message.event }}</span>\n </div>\n <et-query-devtools-json [value]=\"message.data\" />\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No messages received yet.</p>\n }\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No web socket clients registered.</p>\n }\n </div>\n }\n\n <!-- CACHE -->\n @case ('cache') {\n <div class=\"et-query-devtools-scroll\">\n @for (client of cacheView(); track client.name) {\n <div class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.baseUrl || client.name }}</strong>\n <span class=\"et-query-devtools-count\">{{ client.entries.length }} entries</span>\n @if (client.client?.subtle?.persistence) {\n <span class=\"et-query-devtools-count\">{{ persistedCount(client.client!) }} on disk</span>\n <button (click)=\"clearPersistedQueries(client.client!)\" class=\"et-query-devtools-btn\" type=\"button\">\n Clear disk\n </button>\n }\n </div>\n @if (clientFeatures(client.client); as features) {\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features }\" />\n }\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Request</th>\n <th>Consumers</th>\n <th>Secure</th>\n <th>Freshness</th>\n <th>Sync</th>\n <th>Disk</th>\n <th>Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (cacheEntry of client.entries; track cacheEntry.key) {\n <tr>\n <td [title]=\"cacheEntry.request.method + ' ' + cacheEntry.request.url\">\n <span class=\"et-query-devtools-method\">{{ cacheEntry.request.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(cacheEntry.request.url) }}</span>\n </td>\n <td>{{ cacheEntry.consumerCount }}</td>\n <td>{{ cacheEntry.isSecure ? 'yes' : 'no' }}</td>\n <td>{{ cacheFreshness(cacheEntry) }}</td>\n <td>{{ cacheSync(cacheEntry, client.pollStates) }}</td>\n <td>{{ cachePersistence(cacheEntry) }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n <button (click)=\"refetchCacheEntry(cacheEntry)\" class=\"et-query-devtools-btn\" type=\"button\">\n Refetch\n </button>\n <button\n (click)=\"evictCacheEntry(client.repository, cacheEntry.key)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n >\n Evict\n </button>\n </td>\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"7\">Cache empty.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No clients with a cache yet.</p>\n }\n </div>\n }\n\n <!-- TIMELINE -->\n @case ('timeline') {\n @let tl = timeline();\n <div class=\"et-query-devtools-split\">\n <div class=\"et-query-devtools-pane\">\n <div class=\"et-query-devtools-toolbar\">\n <div class=\"et-query-devtools-toolbar-row\">\n @if (inspectFilterIds(); as ids) {\n <span class=\"et-query-devtools-filter-chip\">\n <span class=\"et-query-devtools-filter-icon\" aria-hidden=\"true\">\u2316</span>\n Inspected element\n <span class=\"et-query-devtools-filter-count\">{{ ids.length }}</span>\n </span>\n } @else {\n <label class=\"et-query-devtools-field\">\n <span>Client</span>\n <select\n #timelineClientSelect\n [value]=\"selectedClientName() ?? ''\"\n (change)=\"selectClient(timelineClientSelect.value || null)\"\n class=\"et-query-devtools-select\"\n >\n <option value=\"\">All clients</option>\n @for (name of clientNames(); track name) {\n <option [value]=\"name\">{{ name }}</option>\n }\n </select>\n </label>\n }\n <span class=\"et-query-devtools-count\">\n {{ tl.rows.length }} run{{ tl.rows.length === 1 ? '' : 's' }}\n @if (tl.hidden) {\n \u00B7 {{ tl.hidden }} older not shown\n }\n @if (tl.rows.length) {\n \u00B7 over {{ formatDuration(tl.windowMs) }} from {{ formatTime(tl.startedAt) }}\n }\n </span>\n <button\n [disabled]=\"!tl.rows.length\"\n (click)=\"resetTimeline()\"\n class=\"et-query-devtools-btn et-query-devtools-toolbar-btn\"\n type=\"button\"\n title=\"Clear the run history and the counters of every listed query\"\n >\n \u27F2 Reset\n </button>\n </div>\n </div>\n\n @if (tl.rows.length) {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-timeline\">\n <div class=\"et-query-devtools-timeline-axis\">\n <span class=\"et-query-devtools-timeline-ticks\">\n @for (tick of timelineTicks(); track tick.pct) {\n <span [style.left.%]=\"tick.pct\" class=\"et-query-devtools-timeline-tick\">{{\n tick.label\n }}</span>\n }\n </span>\n </div>\n\n @for (row of tl.rows; track row.key) {\n <button\n [class.et-query-devtools-timeline-row--active]=\"timelineSelectedQueryId() === row.entryId\"\n (click)=\"selectTimelineRow(row)\"\n class=\"et-query-devtools-timeline-row\"\n type=\"button\"\n title=\"{{ row.method }} {{ row.path }} \u00B7 run {{ row.run.index }} \u00B7 {{ row.run.status }}\"\n >\n <span class=\"et-query-devtools-timeline-label\">\n <span class=\"et-query-devtools-method\">{{ row.method }}</span>\n <span class=\"et-query-devtools-url\">{{ row.path }}</span>\n </span>\n <span class=\"et-query-devtools-timeline-track\">\n <span\n [attr.data-status]=\"runStatus(row.run)\"\n [style.left.%]=\"row.leftPct\"\n [style.width.%]=\"row.widthPct\"\n class=\"et-query-devtools-timeline-bar\"\n ></span>\n </span>\n <span class=\"et-query-devtools-timeline-duration\">\n @if (row.durationMs === null) {\n in flight\n } @else {\n {{ formatDuration(row.durationMs) }}\n }\n </span>\n <span class=\"et-query-devtools-timeline-size\">\n @if (row.run.receivedBytes) {\n {{ formatBytes(row.run.receivedBytes) }}\n }\n </span>\n <span class=\"et-query-devtools-timeline-flags\">\n @if (row.run.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Took {{\n row.run.attempts\n }} attempts - so this bar is mostly retry backoff, not one slow round trip\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ row.run.attempts }}\n </span>\n }\n @if (!row.run.didRequest) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Arrived without a request of this query's own - a poll, another consumer of the same cache entry, or another tab\"\n >\n shared\n </span>\n }\n @if (row.run.status === 'aborted') {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"The query requested again before this run's response arrived\"\n >\n aborted\n </span>\n }\n </span>\n </button>\n }\n </div>\n </div>\n } @else {\n <p class=\"et-query-devtools-empty\">\n No requests recorded yet. Every request a listed query makes shows up here as a bar.\n </p>\n }\n </div>\n\n @if (timelineSelectedQuery(); as sel) {\n <aside class=\"et-query-devtools-drawer\">\n <div class=\"et-query-devtools-drawer-head\">\n <span>Query detail</span>\n <button\n (click)=\"timelineSelectedQueryId.set(null)\"\n class=\"et-query-devtools-close\"\n type=\"button\"\n title=\"Close detail\"\n >\n \u2715\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"queryDetailTpl; context: { sel }\" />\n </aside>\n }\n </div>\n }\n\n <!-- EVENTS -->\n @case ('events') {\n <div class=\"et-query-devtools-pane\">\n <div class=\"et-query-devtools-toolbar\">\n <span class=\"et-query-devtools-count\">{{ eventLog().length }} events</span>\n <button (click)=\"clearEvents()\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n <div class=\"et-query-devtools-scroll\">\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Base URL</th>\n <th>Type</th>\n <th>Request</th>\n </tr>\n </thead>\n <tbody>\n @for (event of eventLog(); track event.id) {\n <tr\n [attr.data-status]=\"\n event.type === 'request-error' ? 'error' : event.type === 'queries-refreshed' ? null : 'success'\n \"\n >\n <td>{{ formatTime(event.timestamp) }}</td>\n <td class=\"et-query-devtools-mono\">{{ event.client }}</td>\n <td>\n {{ eventTypeLabel(event) }}\n @if (event.isSecure) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">secure</span>\n }\n </td>\n @if (event.cause; as cause) {\n <td>\n <span class=\"et-query-devtools-url\">{{ causeLabel(cause) }}</span>\n @if (event.refreshed?.length) {\n <div class=\"et-query-devtools-chips\">\n @for (refreshed of event.refreshed; track $index) {\n @if (refreshed.queryIds[0]; as queryId) {\n <button\n (click)=\"selectQuery(queryId)\"\n class=\"et-query-devtools-chip et-query-devtools-chip--button\"\n type=\"button\"\n title=\"Open {{ refreshed.method }} {{ refreshed.path }}\"\n >\n {{ refreshed.method }} {{ refreshed.path }}\n <!-- One request, several queries on the same cache key. Spelling the count\n out beats repeating an identical row per consumer. -->\n @if (refreshed.queryIds.length > 1) {\n <small>\u00D7{{ refreshed.queryIds.length }}</small>\n }\n </button>\n } @else {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"No registered query holds this cache entry any more\"\n >\n {{ refreshed.method }} {{ refreshed.path }}\n </span>\n }\n }\n </div>\n } @else {\n <span class=\"et-query-devtools-hint\">matched no query in use</span>\n }\n </td>\n } @else if (event.url; as url) {\n <td>\n @if (event.queryId) {\n <button\n (click)=\"selectEventRow(event)\"\n class=\"et-query-devtools-row-link\"\n type=\"button\"\n title=\"Open {{ event.method }} {{ url }}\"\n >\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </button>\n } @else {\n <span [title]=\"event.method + ' ' + url\">\n <span class=\"et-query-devtools-method\">{{ event.method }}</span>\n <span class=\"et-query-devtools-url\">{{ requestPath(url) }}</span>\n </span>\n }\n </td>\n } @else {\n <td class=\"et-query-devtools-url\">all secure entries dropped</td>\n }\n </tr>\n } @empty {\n <tr>\n <td class=\"et-query-devtools-empty\" colspan=\"4\">No events captured yet.</td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n }\n\n <!-- FAULTS -->\n @case ('faults') {\n <div class=\"et-query-devtools-scroll\">\n <div class=\"et-query-devtools-toolbar\">\n <p class=\"et-query-devtools-hint\">\n Faults are injected into the request pipeline, so retries, error handling features and the cache see\n them exactly as they would see a real one. Unlike the rest of the panel, what you arm here is not\n persisted - a page reload disarms every client.\n </p>\n </div>\n\n @for (client of faultClients(); track client.name) {\n @let fault = client.fault;\n <div [class.et-query-devtools-card--armed]=\"client.armed\" class=\"et-query-devtools-card\">\n <div class=\"et-query-devtools-card-head\">\n <strong class=\"et-query-devtools-mono\">{{ client.name }}</strong>\n <span class=\"et-query-devtools-url\">{{ client.baseUrl }}</span>\n @if (client.armed) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--error\">armed</span>\n <button (click)=\"CLEAR_FAULTS(client.name)\" class=\"et-query-devtools-btn\" type=\"button\">\n Disarm\n </button>\n }\n </div>\n\n <div class=\"et-query-devtools-faults\">\n <label class=\"et-query-devtools-field\">\n <span>Latency</span>\n <input\n #latencyInput\n [value]=\"fault.latencyMs\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'latencyMs', value: latencyInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"60000\"\n step=\"100\"\n />\n <small>ms before every attempt starts</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Fail next</span>\n <input\n #failNextInput\n [value]=\"fault.failNext\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'failNext', value: failNextInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"99\"\n step=\"1\"\n />\n <small>attempts, counted down as they are spent</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Fail rate</span>\n <input\n #failRateInput\n [value]=\"fault.failRate\"\n (change)=\"\n armFaultValue({ clientName: client.name, field: 'failRate', value: failRateInput.value })\n \"\n class=\"et-query-devtools-input et-query-devtools-input--number\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"5\"\n />\n <small>% of attempts, rolled per attempt</small>\n </label>\n\n <label class=\"et-query-devtools-field\">\n <span>Status</span>\n <select\n #statusSelect\n (change)=\"armFaultStatus({ clientName: client.name, value: statusSelect.value })\"\n class=\"et-query-devtools-select\"\n >\n @for (option of FAULT_STATUSES; track option.status) {\n <!-- `selected` on the option, not `value` on the select: the select's value is\n applied before its options exist, so it would fall back to the first one. -->\n <option [value]=\"option.status\" [selected]=\"option.status === fault.status\">\n {{ option.status }} \u00B7 {{ option.label }}\n </option>\n }\n </select>\n <small>\n @if (isFaultStatusRetryable(fault.status)) {\n the default retry policy retries this\n } @else {\n the default retry policy gives up on this\n }\n </small>\n </label>\n </div>\n </div>\n } @empty {\n <p class=\"et-query-devtools-empty\">No query clients registered yet.</p>\n }\n </div>\n }\n }\n </div>\n\n <!-- A route with its path params and query string picked out from the static path. The segments must\n stay element-only: a text node between them would render as a space inside the route. -->\n <ng-template #routeTpl let-segments=\"segments\" let-fallback=\"fallback\">\n @for (segment of segments; track $index) {\n <span [class]=\"'et-query-devtools-route-' + segment.kind\" [attr.title]=\"segment.name\">{{ segment.text }}</span>\n } @empty {\n {{ fallback }}\n }\n </ng-template>\n\n <!-- Reusable dt/dd pair for a card's `et-query-devtools-meta` list: what its queries ran and moved. -->\n <ng-template #activityRowTpl let-activity=\"activity\">\n <dt>Traffic</dt>\n <dd>\n @if (activity.hasActivity) {\n {{ activity.stats.requests }} request{{ activity.stats.requests === 1 ? '' : 's' }} \u00B7 \u2193\n {{ formatTransferred(activity.stats.receivedBytes, activity.stats.hasEstimatedBytes) }}\n @if (activity.stats.sentBytes) {\n \u00B7 \u2191 {{ formatTransferred(activity.stats.sentBytes, activity.stats.hasEstimatedBytes) }}\n }\n @if (activity.cacheServed) {\n \u00B7 {{ activity.cacheServed }} from cache\n }\n @if (activity.stats.errors) {\n \u00B7 {{ activity.stats.errors }} failed\n }\n } @else {\n \u2014\n }\n </dd>\n </ng-template>\n\n <!-- Reusable feature list: the feature's name, followed by the options it was configured with. -->\n <ng-template #featuresTpl let-features=\"features\">\n <ul class=\"et-query-devtools-features\">\n @for (feature of features; track feature.type) {\n <li class=\"et-query-devtools-feature\">\n <span class=\"et-query-devtools-chip\">{{ featureLabel(feature.type) }}</span>\n <!-- Always rendered: the list is one grid, so a feature without options still needs its cell\n for the next row's chip to stay in the same column. -->\n <span class=\"et-query-devtools-feature-details\">\n @for (detail of feature.details; track detail.label) {\n <span class=\"et-query-devtools-feature-detail\">\n <span class=\"et-query-devtools-feature-label\">{{ detail.label }}</span>\n <span class=\"et-query-devtools-feature-value\">{{ detail.value }}</span>\n </span>\n }\n </span>\n </li>\n }\n </ul>\n </ng-template>\n\n <!-- Reusable inner-query list (stacks) \u2014 clicking opens the split-view drawer, not a tab switch. -->\n <ng-template #innerQueriesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track $index) {\n <button\n [class.et-query-devtools-inner-row--active]=\"stackSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"stackSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"\n routeTpl;\n context: { segments: link.segments, fallback: link.query.id() || 'query ' + ($index + 1) }\n \"\n />\n </span>\n @let linkStats = linkActivity(link);\n @if (linkStats.hasActivity) {\n <span class=\"et-query-devtools-traffic\">\n {{ formatTransferred(linkStats.stats.receivedBytes, linkStats.stats.hasEstimatedBytes) }}\n </span>\n }\n </button>\n }\n </div>\n </ng-template>\n\n <!-- The queries a form feeds \u2014 clicking one opens the split-view drawer, not a tab switch. -->\n <ng-template #formDrivesTpl let-links=\"links\">\n <div class=\"et-query-devtools-inner-queries\">\n @for (link of links; track link.id) {\n <button\n [class.et-query-devtools-inner-row--active]=\"formSelectedQueryId() === link.id\"\n [disabled]=\"!link.id\"\n (click)=\"formSelectedQueryId.set(link.id)\"\n class=\"et-query-devtools-inner-row\"\n type=\"button\"\n >\n <span [attr.data-status]=\"queryStatus(link.query)\" class=\"et-query-devtools-status\"></span>\n <span class=\"et-query-devtools-method\">{{ link.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: link.segments, fallback: link.query.id() || 'query' }\"\n />\n </span>\n </button>\n }\n </div>\n </ng-template>\n\n <!-- Reusable query detail \u2014 used by the Queries tab and by every split-view drawer. -->\n <ng-template #queryDetailTpl let-sel=\"sel\">\n <div class=\"et-query-devtools-detail-head\">\n <span class=\"et-query-devtools-method\">{{ sel.entry.meta.method }}</span>\n <span class=\"et-query-devtools-route\">\n <ng-container\n *ngTemplateOutlet=\"routeTpl; context: { segments: routeSegments(sel.entry, sel.query), fallback: '\u2014' }\"\n />\n </span>\n </div>\n\n @if (requestProgress(sel.query); as live) {\n <div class=\"et-query-devtools-live\">\n @if (live.retry; as retry) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"The retry policy is backing off before it tries again. A query stuck loading with this chip is waiting, not slow.\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n attempt {{ retry.attempt }} in {{ formatCountdown(live.retryInMs) }}\n </span>\n <span class=\"et-query-devtools-live-note\">\n {{ retryCause(retry.status) }} \u00B7 backing off {{ formatDuration(retry.delayMs) }}\n </span>\n } @else if (live.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"How many HTTP attempts the last execution took - the retry policy fired for the rest\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ live.attempts }} attempts\n </span>\n }\n\n @if (live.progress; as progress) {\n <span\n [attr.aria-valuenow]=\"progress.percentage\"\n class=\"et-query-devtools-progress\"\n role=\"progressbar\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-label=\"Transfer progress\"\n >\n <span [style.inline-size.%]=\"progress.percentage\" class=\"et-query-devtools-progress-bar\"></span>\n </span>\n <span class=\"et-query-devtools-live-note\">\n {{ formatPercent(progress.percentage) }} \u00B7 {{ formatBytes(progress.loaded) }} of\n {{ formatBytes(progress.total) }}\n @if (progress.remainingTime !== null) {\n \u00B7 {{ formatDuration(progress.remainingTime) }} left\n }\n </span>\n }\n </div>\n }\n\n <div class=\"et-query-devtools-actions\">\n <span class=\"et-query-devtools-actions-label\">Run</span>\n <button (click)=\"executeQuery(sel.query, false)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"executeQuery(sel.query, true)\" class=\"et-query-devtools-btn\" type=\"button\">Cached</button>\n <button (click)=\"resetQuery(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Reset</button>\n <button (click)=\"copyReport(sel.entry, sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">\n {{ copiedReport() ? '\u2713 Copied' : 'Copy report' }}\n </button>\n <button\n (click)=\"copyInsomniaRequest(sel.entry, sel.query)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Copy as an Insomnia collection (Import \u2192 From Clipboard). Includes the request headers, and the token refresh for a secure query.\"\n >\n {{ copiedInsomnia() ? '\u2713 Copied' : 'Insomnia' }}\n </button>\n <span class=\"et-query-devtools-actions-label\">Edit</span>\n <button (click)=\"openResponseEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Response</button>\n <button (click)=\"openArgsEditor(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Replay args</button>\n <span class=\"et-query-devtools-actions-label\">Force</span>\n <button (click)=\"forceLoading(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Loading</button>\n <button (click)=\"forceError(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Error</button>\n <button (click)=\"forceEmpty(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Empty</button>\n <button (click)=\"clearForced(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Clear</button>\n </div>\n\n @if (editorMode() !== 'none') {\n <div class=\"et-query-devtools-editor\">\n @if (editorMode() === 'response') {\n <h4 class=\"et-query-devtools-section\">Edit response \u2192 setResponse()</h4>\n <textarea\n #respArea\n [value]=\"editorSeed\"\n (input)=\"responseDraft.set(respArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyResponse(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Apply</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n } @else {\n <h4 class=\"et-query-devtools-section\">Replay with edited args \u2192 execute()</h4>\n <textarea\n #argsArea\n [value]=\"editorSeed\"\n (input)=\"argsDraft.set(argsArea.value)\"\n class=\"et-query-devtools-textarea\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"et-query-devtools-actions\">\n <button (click)=\"applyArgs(sel.query)\" class=\"et-query-devtools-btn\" type=\"button\">Execute</button>\n <button (click)=\"cancelEditor()\" class=\"et-query-devtools-btn\" type=\"button\">Cancel</button>\n </div>\n }\n @if (editError(); as err) {\n <p class=\"et-query-devtools-editor-error\">{{ err }}</p>\n }\n </div>\n }\n\n @let detailRuns = queryRuns(sel.entry);\n <nav class=\"et-query-devtools-subtabs\" role=\"tablist\">\n @for (tab of detailTabs; track tab.id) {\n <button\n [class.et-query-devtools-subtab--active]=\"detailTab() === tab.id\"\n [attr.aria-selected]=\"detailTab() === tab.id\"\n (click)=\"detailTab.set(tab.id)\"\n class=\"et-query-devtools-subtab\"\n type=\"button\"\n role=\"tab\"\n >\n {{ tab.label }}\n @if (tab.id === 'history' && detailRuns.length) {\n <span class=\"et-query-devtools-tab-badge\">{{ detailRuns.length }}</span>\n }\n <!-- The error body lives under Data, so the tab has to say so - a failure must not hide\n behind a tab that is not open. -->\n @if (tab.id === 'data' && sel.query.error()) {\n <span class=\"et-query-devtools-tab-badge et-query-devtools-tab-badge--error\">!</span>\n }\n </button>\n }\n </nav>\n\n @switch (detailTab()) {\n @case ('overview') {\n <dl class=\"et-query-devtools-meta\">\n <dt>Base URL</dt>\n <dd class=\"et-query-devtools-mono\">\n {{ sel.entry.meta.clientBaseUrl || sel.entry.meta.clientName || '\u2014' }}\n </dd>\n <dt>Route</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.entry.meta.route || '\u2014' }}</dd>\n <dt>Request URL</dt>\n <dd class=\"et-query-devtools-mono\">{{ requestUrl(sel.query) || '\u2014' }}</dd>\n <dt>Status</dt>\n <dd>{{ queryStatus(sel.query) }}</dd>\n <dt>Cache key</dt>\n <dd class=\"et-query-devtools-mono\">{{ sel.query.id() || '\u2014' }}</dd>\n <dt>Last executed</dt>\n <dd>{{ formatTime(sel.query.lastTimeExecutedAt()) }}</dd>\n <dt>Triggered by</dt>\n <dd>{{ sel.query.triggeredBy() || 'user' }}</dd>\n @let refreshes = refreshesFor(sel.entry.id);\n @if (refreshes.length) {\n <dt\n title=\"Invalidations and mutation refreshes that re-executed this query, as far back as the event log goes\"\n >\n Refetched by\n </dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (refresh of refreshes.slice(0, 5); track refresh.id) {\n <span class=\"et-query-devtools-chip et-query-devtools-chip--muted\">\n {{ formatTime(refresh.timestamp) }} \u00B7 {{ refresh.label }}\n </span>\n }\n @if (refreshes.length > 5) {\n <span class=\"et-query-devtools-hint\">+{{ refreshes.length - 5 }} earlier</span>\n }\n </div>\n </dd>\n }\n @let drivers = formsDrivingQuery(sel.entry);\n @if (drivers.length) {\n <dt>Args from</dt>\n <dd>\n <div class=\"et-query-devtools-chips\">\n @for (driver of drivers; track driver.id) {\n <button\n (click)=\"selectForm(driver.id)\"\n class=\"et-query-devtools-chip et-query-devtools-chip--button\"\n type=\"button\"\n title=\"Open the query form in the Forms tab\"\n >\n {{ driver.meta.name }}\n </button>\n }\n </div>\n </dd>\n }\n @if (sel.entry.meta.features?.length) {\n <dt>Features</dt>\n <dd>\n <ng-container *ngTemplateOutlet=\"featuresTpl; context: { features: sel.entry.meta.features }\" />\n </dd>\n }\n </dl>\n\n @let activity = queryActivity(sel.entry);\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Activity</h4>\n <button\n [disabled]=\"!activity.hasActivity\"\n (click)=\"resetStats(sel.entry)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Clear the counters and the run history, so the next interaction can be measured on its own\"\n >\n \u27F2 Reset\n </button>\n </div>\n\n <dl class=\"et-query-devtools-stats\">\n <div class=\"et-query-devtools-stat\" title=\"Every time the query ran, cache hits included\">\n <dt>Refreshes</dt>\n <dd>{{ activity.stats.executions }}</dd>\n @if (activity.cacheServed) {\n <span class=\"et-query-devtools-stat-sub\">{{ activity.cacheServed }} without a request</span>\n }\n </div>\n <div class=\"et-query-devtools-stat\" title=\"Executions that started a request, and how they ended\">\n <dt>Requests</dt>\n <dd>{{ activity.stats.requests }}</dd>\n <span class=\"et-query-devtools-stat-sub\">\n {{ activity.stats.responses }} ok\n @if (activity.stats.errors) {\n \u00B7 {{ activity.stats.errors }} failed\n }\n </span>\n </div>\n <div\n class=\"et-query-devtools-stat\"\n title=\"Total response payload received. Sizes without a content-length header are measured from the decoded body, so they ignore compression.\"\n >\n <dt>Received</dt>\n <dd>{{ formatTransferred(activity.stats.receivedBytes, activity.stats.hasEstimatedBytes) }}</dd>\n @if (activity.avgResponseBytes !== null) {\n <span class=\"et-query-devtools-stat-sub\"\n >{{ formatBytes(activity.avgResponseBytes) }} per response</span\n >\n }\n </div>\n @if (activity.stats.sentBytes) {\n <div class=\"et-query-devtools-stat\" title=\"Total request body sent (headers not counted)\">\n <dt>Sent</dt>\n <dd>{{ formatTransferred(activity.stats.sentBytes, activity.stats.hasEstimatedBytes) }}</dd>\n </div>\n }\n @if (activity.stats.retries) {\n <div\n class=\"et-query-devtools-stat\"\n title=\"Attempts the retry policy added on top of the first one. Which run they belong to is under History.\"\n >\n <dt>Retries</dt>\n <dd>{{ activity.stats.retries }}</dd>\n </div>\n }\n <div class=\"et-query-devtools-stat\" title=\"Measured from the execution that preceded each response\">\n <dt>Duration</dt>\n <dd>{{ formatDuration(activity.stats.lastDurationMs) }}</dd>\n @if (activity.avgDurationMs !== null) {\n <span class=\"et-query-devtools-stat-sub\">{{ formatDuration(activity.avgDurationMs) }} on average</span>\n }\n </div>\n <div class=\"et-query-devtools-stat\">\n <dt>Last response</dt>\n <dd class=\"et-query-devtools-stat-value--sm\">{{ formatTime(activity.stats.lastResponseAt) }}</dd>\n @if (activity.stats.firstExecutedAt !== null) {\n <span class=\"et-query-devtools-stat-sub\"\n >first ran {{ formatTime(activity.stats.firstExecutedAt) }}</span\n >\n }\n </div>\n </dl>\n }\n @case ('history') {\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Runs</h4>\n <button\n [disabled]=\"!detailRuns.length\"\n (click)=\"resetStats(sel.entry)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Clear the run history and the counters, so the next interaction can be measured on its own\"\n >\n \u27F2 Reset\n </button>\n </div>\n @if (detailRuns.length) {\n <table class=\"et-query-devtools-table\">\n <thead>\n <tr>\n <th>Run</th>\n <th>Started</th>\n <th>Duration</th>\n <th>Received</th>\n <th>Diff</th>\n </tr>\n </thead>\n <tbody>\n @for (run of detailRuns; track run.index) {\n <tr [attr.data-status]=\"run.status === 'error' ? 'error' : null\">\n <td class=\"et-query-devtools-run-cell\">\n <span\n [attr.data-status]=\"runStatus(run)\"\n [title]=\"run.status\"\n class=\"et-query-devtools-status\"\n ></span>\n #{{ run.index }}\n @if (run.attempts > 1) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--loading\"\n title=\"Took {{\n run.attempts\n }} attempts - the duration next to it covers every one of them, backoff included\"\n >\n <span class=\"et-query-devtools-retry-glyph\" aria-hidden=\"true\">\u27F3</span>\n {{ run.attempts }}\n </span>\n }\n @if (!run.didRequest) {\n <span\n class=\"et-query-devtools-chip et-query-devtools-chip--muted\"\n title=\"Arrived without a request of this query's own - a poll, another consumer of the same cache entry, or another tab\"\n >\n shared\n </span>\n }\n </td>\n <td>{{ formatTime(run.startedAt) }}</td>\n <td>\n @if (run.endedAt === null) {\n in flight\n } @else {\n {{ formatDuration(run.endedAt - run.startedAt) }}\n }\n </td>\n <td>{{ run.receivedBytes ? formatBytes(run.receivedBytes) : '\u2014' }}</td>\n <td class=\"et-query-devtools-cell-actions\">\n @if (canDiffRun(sel.entry, run)) {\n <button\n [class.et-query-devtools-btn--active]=\"diffRunIndex() === run.index\"\n (click)=\"toggleRunDiff(run)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Compare this response against the previous one still held\"\n >\n Diff\n </button>\n } @else if (run.hasResponse) {\n <span class=\"et-query-devtools-count\">nothing earlier to diff</span>\n } @else if (run.status === 'success') {\n <span class=\"et-query-devtools-count\">body no longer held</span>\n } @else {\n <span class=\"et-query-devtools-count\">\u2014</span>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n\n @if (responseDiff(sel.entry); as comparison) {\n <div class=\"et-query-devtools-section-row\">\n <h5 class=\"et-query-devtools-section\">\n Response diff \u00B7 run #{{ comparison.before.index }} \u2192 #{{ comparison.after.index }}\n </h5>\n <span class=\"et-query-devtools-count\">\n @if (comparison.diff.entries.length) {\n {{ comparison.diff.entries.length }} path{{ comparison.diff.entries.length === 1 ? '' : 's' }}\n @if (comparison.diff.truncated) {\n (capped)\n }\n } @else {\n identical\n }\n </span>\n </div>\n @if (comparison.diff.entries.length) {\n <table class=\"et-query-devtools-table et-query-devtools-diff\">\n <thead>\n <tr>\n <th>Path</th>\n <th>Before</th>\n <th>After</th>\n </tr>\n </thead>\n <tbody>\n @for (change of comparison.diff.entries; track change.path) {\n <tr [attr.data-kind]=\"change.kind\">\n <td class=\"et-query-devtools-mono\">{{ change.path }}</td>\n <td class=\"et-query-devtools-mono et-query-devtools-diff-before\">\n {{ change.kind === 'added' ? '\u2014' : inlineValue(change.before) }}\n </td>\n <td class=\"et-query-devtools-mono et-query-devtools-diff-after\">\n {{ change.kind === 'removed' ? '\u2014' : inlineValue(change.after) }}\n </td>\n </tr>\n }\n </tbody>\n </table>\n } @else {\n <p class=\"et-query-devtools-empty\">The two responses are identical.</p>\n }\n }\n } @else {\n <p class=\"et-query-devtools-empty\">No runs recorded yet.</p>\n }\n }\n @case ('data') {\n @if (sel.entry.meta.gqlQuery; as gqlQuery) {\n <div class=\"et-query-devtools-section-row\">\n <h5 class=\"et-query-devtools-section\">GraphQL document</h5>\n <button\n (click)=\"copyGqlDocument(gqlQuery)\"\n class=\"et-query-devtools-btn\"\n type=\"button\"\n title=\"Copy the GraphQL document\"\n >\n {{ copiedGql() ? '\u2713 Copied' : '\u29C9 Copy' }}\n </button>\n </div>\n <pre class=\"et-query-devtools-code\">{{ gqlDocument(gqlQuery) }}</pre>\n }\n\n <div class=\"et-query-devtools-section-row\">\n <h4 class=\"et-query-devtools-section\">Value explorer</h4>\n <input\n #jsonSearchInput\n [value]=\"jsonSearch()\"\n (input)=\"jsonSearch.set(jsonSearchInput.value)\"\n class=\"et-query-devtools-search\"\n type=\"search\"\n placeholder=\"filter keys / values\u2026\"\n />\n </div>\n\n <h5 class=\"et-query-devtools-section\">Args</h5>\n <et-query-devtools-json\n [value]=\"queryArgs(sel.query)\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"args\"\n />\n\n @if (sel.query.error(); as error) {\n <h5 class=\"et-query-devtools-section\">Error ({{ error.raw.status }})</h5>\n <et-query-devtools-json\n [value]=\"error.isList ? error.errors : error.error\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"error\"\n />\n } @else {\n <h5 class=\"et-query-devtools-section\">Response</h5>\n <et-query-devtools-json\n [value]=\"sel.query.response()\"\n [search]=\"jsonSearchTerm()\"\n [expandedPaths]=\"jsonExpandedPaths()\"\n [collapsedPaths]=\"jsonCollapsedPaths()\"\n [toggleFn]=\"toggleJsonPath\"\n path=\"response\"\n />\n }\n }\n }\n </ng-template>\n </section>\n}\n\n@if (!open()) {\n <et-query-devtools-toggle (openChange)=\"toggleOpen()\" />\n}\n\n@if (inspectActive() && inspectHover(); as hover) {\n <div\n [style.left.px]=\"hover.rect.left\"\n [style.top.px]=\"hover.rect.top\"\n [style.width.px]=\"hover.rect.width\"\n [style.height.px]=\"hover.rect.height\"\n class=\"et-query-devtools-inspect-box\"\n >\n <span class=\"et-query-devtools-inspect-tag\">{{ inspectLabel(hover.entries) }}</span>\n </div>\n}\n", styles: ["@layer components{.et-query-devtools-host{--_et-qdt-bg: var(--et-surface-background-solid, rgb(24 24 27));--_et-qdt-fg: var(--et-surface-color-solid, rgb(244 244 245));--_et-qdt-muted: var(--et-surface-color-muted-solid, rgb(161 161 170));--_et-qdt-subtle: var(--et-surface-color-subtle-solid, rgb(113 113 122));--_et-qdt-border: var(--et-surface-border-solid, rgb(255 255 255 / .12));--_et-qdt-interaction: var(--et-surface-interaction-solid, rgb(255 255 255));--_et-qdt-accent: var(--et-theme-color-primary-solid, rgb(96 165 250));--_et-qdt-hover: color-mix(in srgb, var(--_et-qdt-interaction) 10%, transparent);--_et-qdt-active: color-mix(in srgb, var(--_et-qdt-interaction) 18%, transparent);--_et-qdt-success: rgb(34 197 94);--_et-qdt-error: rgb(239 68 68);--_et-qdt-loading: rgb(234 179 8);position:fixed;inset-inline:0;inset-block-end:0;z-index:2147483000;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,Liberation Mono,monospace;font-size:1.2rem;line-height:1.4;pointer-events:none}.et-query-devtools-host>*{pointer-events:auto}.et-query-devtools-panel{position:relative;display:flex;flex-direction:column;inline-size:100%;min-block-size:20rem;max-block-size:90vh;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border-block-start:1px solid var(--_et-qdt-border);box-shadow:0 -.4rem 2rem #00000059}.et-query-devtools-panel--resizing{-webkit-user-select:none;user-select:none}.et-query-devtools-resize{position:absolute;inset-inline:0;inset-block-start:0;block-size:.6rem;cursor:ns-resize;z-index:1}.et-query-devtools-resize:after{content:\"\";position:absolute;inset-inline:50%;inset-block-start:.2rem;inline-size:4rem;margin-inline-start:-2rem;block-size:.2rem;border-radius:999px;background:var(--_et-qdt-border)}.et-query-devtools-resize:hover:after{background:var(--_et-qdt-accent)}.et-query-devtools-header{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding-inline:.8rem;border-block-end:1px solid var(--_et-qdt-border);flex:0 0 auto}.et-query-devtools-armed-bar{display:flex;align-items:center;gap:.8rem;flex:0 0 auto;padding:.6rem .8rem;background:color-mix(in srgb,var(--_et-qdt-error) 14%,transparent);border-block-end:1px solid color-mix(in srgb,var(--_et-qdt-error) 45%,transparent);color:var(--_et-qdt-error);font-size:1.1rem}.et-query-devtools-armed-text{flex:1 1 auto;min-inline-size:0}.et-query-devtools-armed-dot{flex:0 0 auto;inline-size:.8rem;block-size:.8rem;border-radius:50%;background:var(--_et-qdt-error);animation:et-query-devtools-armed-pulse 1.6s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.et-query-devtools-armed-dot{animation:none}}@keyframes et-query-devtools-armed-pulse{50%{opacity:.25}}.et-query-devtools-armed-bar .et-query-devtools-btn{flex:0 0 auto;border-color:color-mix(in srgb,var(--_et-qdt-error) 55%,transparent);color:var(--_et-qdt-error)}.et-query-devtools-tabs{display:flex;flex-wrap:wrap;gap:.2rem}.et-query-devtools-tab{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);padding:.9rem 1rem;cursor:pointer;border-block-end:2px solid transparent;font:inherit}.et-query-devtools-tab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-tab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-tab-badge{display:inline-block;margin-inline-start:.5rem;padding-inline:.45rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-interaction) 12%,transparent);color:var(--_et-qdt-muted);font-size:1rem;font-variant-numeric:tabular-nums}.et-query-devtools-tab-badge--error{background:color-mix(in srgb,var(--_et-qdt-error) 25%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-header-actions{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}.et-query-devtools-inspect-btn{appearance:none;position:relative;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.4rem .8rem;font:inherit;transition:color .12s ease,background-color .12s ease,border-color .12s ease}.et-query-devtools-inspect-btn:hover{color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent)}.et-query-devtools-inspect-btn:focus-visible{outline:2px solid var(--_et-qdt-accent);outline-offset:2px}.et-query-devtools-inspect-icon{inline-size:1.4rem;block-size:1.4rem;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round}.et-query-devtools-inspect-icon-dot{fill:currentColor;stroke:none}.et-query-devtools-inspect-btn--active,.et-query-devtools-inspect-btn--active:hover{color:#09090b;background:var(--_et-qdt-accent);border-color:var(--_et-qdt-accent)}.et-query-devtools-inspect-btn--active:after{content:\"\";position:absolute;inset:-.3rem;border:1px solid var(--_et-qdt-accent);border-radius:.6rem;pointer-events:none;animation:et-query-devtools-inspect-pulse 1.4s ease-in-out infinite}.et-query-devtools-inspect-btn--active .et-query-devtools-inspect-icon-ring{animation:et-query-devtools-inspect-pulse 1.4s ease-in-out infinite}@keyframes et-query-devtools-inspect-pulse{0%,to{opacity:.2}50%{opacity:1}}@media(prefers-reduced-motion:reduce){.et-query-devtools-inspect-btn{transition:none}.et-query-devtools-inspect-btn--active:after,.et-query-devtools-inspect-btn--active .et-query-devtools-inspect-icon-ring{animation:none;opacity:1}}.et-query-devtools-close{appearance:none;border:none;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.6rem;font:inherit}.et-query-devtools-close-panel{appearance:none;display:inline-flex;align-items:center;gap:.6rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:var(--_et-qdt-hover);color:var(--_et-qdt-fg);cursor:pointer;padding:.4rem .6rem .4rem .8rem;font:inherit}.et-query-devtools-close-panel:hover{border-color:var(--_et-qdt-error);color:var(--_et-qdt-error)}.et-query-devtools-kbd{border-radius:.3rem;background:var(--_et-qdt-hover);box-shadow:inset 0 0 0 1px var(--_et-qdt-border);color:var(--_et-qdt-muted);padding:.15rem .4rem;font:inherit;font-size:1.1rem;line-height:1.2;white-space:nowrap}.et-query-devtools-kbd--inverse{background:#00000026;box-shadow:inset 0 0 0 1px #00000040;color:inherit}.et-query-devtools-inspect-box{position:fixed;z-index:2147483002;pointer-events:none;border:2px solid var(--_et-qdt-accent);background:color-mix(in srgb,var(--_et-qdt-accent) 15%,transparent);border-radius:.2rem}.et-query-devtools-inspect-tag{position:absolute;inset-block-start:100%;inset-inline-start:0;margin-block-start:.2rem;padding:.2rem .5rem;border-radius:.3rem;background:var(--_et-qdt-accent);color:#09090b;font-family:ui-monospace,monospace;font-size:1.1rem;font-weight:600;white-space:nowrap}.et-query-devtools-close:hover{color:var(--_et-qdt-fg)}.et-query-devtools-body{flex:1 1 auto;min-block-size:0;overflow:hidden}.et-query-devtools-master-detail{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-list{flex:0 0 34%;min-inline-size:22rem;max-inline-size:40rem;overflow-y:auto;border-inline-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail{flex:1 1 auto;min-inline-size:0;overflow-y:auto;padding:1rem}.et-query-devtools-detail .et-query-devtools-detail-head,.et-query-devtools-drawer .et-query-devtools-detail-head{padding-block-end:.8rem;border-block-end:1px solid var(--_et-qdt-border)}.et-query-devtools-detail .et-query-devtools-meta,.et-query-devtools-drawer .et-query-devtools-meta{padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-detail .et-query-devtools-section-row,.et-query-devtools-drawer .et-query-devtools-section-row{padding-block-start:.8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-filter-chip{display:inline-flex;align-items:center;gap:.6rem;padding:.3rem .35rem .3rem .7rem;border-radius:999px;background:color-mix(in srgb,var(--_et-qdt-accent) 16%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_et-qdt-accent) 45%,transparent);color:var(--_et-qdt-accent);font-size:1.1rem;font-weight:600;letter-spacing:.02em}.et-query-devtools-filter-icon{font-size:1.3rem;line-height:1}.et-query-devtools-filter-count{border-radius:999px;background:var(--_et-qdt-accent);color:#09090b;min-inline-size:1.8rem;padding:0 .5rem;text-align:center}.et-query-devtools-filter-clear{appearance:none;display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:.3rem .6rem;font:inherit;font-size:1.15rem;line-height:1.4}.et-query-devtools-filter-clear:hover{border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent);color:var(--_et-qdt-fg)}.et-query-devtools-scroll{block-size:100%;overflow:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.et-query-devtools-split{display:flex;block-size:100%;min-block-size:0}.et-query-devtools-split .et-query-devtools-scroll{flex:1 1 auto;min-inline-size:0}.et-query-devtools-pane{display:flex;flex-direction:column;flex:1 1 auto;block-size:100%;min-inline-size:0;min-block-size:0;overflow:hidden}.et-query-devtools-pane>.et-query-devtools-scroll{padding-block:0 1rem}.et-query-devtools-drawer{flex:0 0 42%;min-inline-size:26rem;max-inline-size:48rem;overflow-y:auto;padding:0 1rem 1rem;border-inline-start:1px solid var(--_et-qdt-border)}.et-query-devtools-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:.8rem;padding:.8rem 0;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg);color:var(--_et-qdt-muted);border-block-end:1px solid var(--_et-qdt-border);margin-block-end:.8rem}.et-query-devtools-toolbar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.8rem;padding:.8rem;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-toolbar-row{display:flex;align-items:center;flex-wrap:wrap;flex:1 1 100%;gap:.8rem}.et-query-devtools-toolbar-row .et-query-devtools-search{flex:1 1 100%}.et-query-devtools-toolbar--filtered{background:linear-gradient(to right,color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent),transparent 65%),var(--_et-qdt-bg);box-shadow:inset .2rem 0 0 var(--_et-qdt-accent),inset 0 -1px color-mix(in srgb,var(--_et-qdt-accent) 30%,transparent)}.et-query-devtools-field{display:inline-flex;align-items:center;gap:.6rem;color:var(--_et-qdt-muted)}.et-query-devtools-select{background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-count{color:var(--_et-qdt-subtle)}.et-query-devtools-row{display:flex;align-items:center;gap:.8rem;inline-size:100%;min-block-size:3.6rem;text-align:start;appearance:none;border:none;border-block-end:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-fg);padding:.8rem;cursor:pointer;font:inherit}.et-query-devtools-row>.et-query-devtools-chip{padding-block:.1rem;line-height:1.2}.et-query-devtools-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-row--active{background:var(--_et-qdt-active)}.et-query-devtools-method{font-weight:700;font-size:1.1rem;color:var(--_et-qdt-muted);white-space:nowrap}.et-query-devtools-route{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-inline-size:0}.et-query-devtools-route-param{color:var(--_et-qdt-accent)}.et-query-devtools-route-query{color:var(--_et-qdt-muted)}.et-query-devtools-status{inline-size:.8rem;block-size:.8rem;border-radius:999px;flex:0 0 auto;background:var(--_et-qdt-subtle)}.et-query-devtools-status[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-status[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-status[data-status=loading],.et-query-devtools-status[data-status=running]{background:var(--_et-qdt-loading)}.et-query-devtools-detail-head{display:flex;align-items:center;gap:.8rem;margin-block-end:.8rem}.et-query-devtools-live{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-live-note{color:var(--_et-qdt-muted);font-size:1.15rem}.et-query-devtools-progress{display:block;flex:1 1 12rem;min-inline-size:8rem;max-inline-size:24rem;block-size:.5rem;border-radius:999px;overflow:hidden;background:var(--_et-qdt-border)}.et-query-devtools-progress-bar{display:block;block-size:100%;border-radius:inherit;background:var(--_et-qdt-accent)}.et-query-devtools-actions{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem;margin-block-end:.8rem}.et-query-devtools-btn{appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.3rem;background:transparent;color:var(--_et-qdt-fg);padding:.25rem .55rem;font-size:1.15rem;line-height:1.4;cursor:pointer}.et-query-devtools-btn:hover{background:var(--_et-qdt-hover)}.et-query-devtools-btn:disabled{color:var(--_et-qdt-subtle);cursor:default;background:transparent}.et-query-devtools-toolbar-btn{margin-inline-start:auto;flex:0 0 auto}.et-query-devtools-meta{display:grid;grid-template-columns:auto 1fr;gap:.3rem 1rem;margin:0 0 1rem}.et-query-devtools-meta dt{color:var(--_et-qdt-muted)}.et-query-devtools-meta dd{margin:0;overflow-wrap:anywhere}.et-query-devtools-section{margin:1rem 0 .4rem;font-size:1.1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:.6rem;margin:0 0 1rem}.et-query-devtools-stat{padding:.6rem .8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 3%,transparent)}.et-query-devtools-stat dt{font-size:1rem;text-transform:uppercase;letter-spacing:.05em;color:var(--_et-qdt-muted)}.et-query-devtools-stat dd{margin:0;font-size:1.5rem;font-variant-numeric:tabular-nums}.et-query-devtools-stat-value--sm{font-size:1.2rem}.et-query-devtools-stat-sub{display:block;font-size:1rem;color:var(--_et-qdt-subtle)}.et-query-devtools-traffic{margin-inline-start:auto;flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--_et-qdt-subtle)}.et-query-devtools-json{margin:0;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere;max-block-size:24rem;overflow-y:auto}.et-query-devtools-json--error{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-mono{font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-code{margin:0 0 .8rem;padding:.8rem;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1.15rem;max-block-size:20rem}.et-query-devtools-ws-message{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;margin-block-end:.4rem}.et-query-devtools-url{display:inline-block;max-inline-size:32rem;margin-inline-start:.6rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.et-query-devtools-chips,.et-query-devtools-chip-row{display:flex;flex-wrap:wrap;gap:.4rem;margin-block-end:.8rem}.et-query-devtools-features{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:center;gap:.4rem .8rem;margin:0 0 .8rem;padding:0;list-style:none}.et-query-devtools-meta .et-query-devtools-features{margin-block-end:0}.et-query-devtools-feature{display:contents}.et-query-devtools-feature>.et-query-devtools-chip{justify-self:start}.et-query-devtools-feature-details{display:flex;flex-wrap:wrap;gap:.3rem}.et-query-devtools-feature-detail{display:inline-flex;align-items:baseline;gap:.4rem;padding:.2rem .5rem;border-radius:.4rem;background:color-mix(in srgb,var(--_et-qdt-interaction) 5%,transparent);font-size:1.1rem}.et-query-devtools-feature-label{color:var(--_et-qdt-muted)}.et-query-devtools-feature-value{color:var(--_et-qdt-fg);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.et-query-devtools-chip{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .6rem;border-radius:999px;border:1px solid var(--_et-qdt-border);font-size:1.1rem;color:var(--_et-qdt-fg);background:color-mix(in srgb,var(--_et-qdt-interaction) 6%,transparent)}.et-query-devtools-chip--muted{color:var(--_et-qdt-muted)}.et-query-devtools-retry-glyph{font-size:.7em;line-height:1}.et-query-devtools-chip--loading{color:var(--_et-qdt-loading);border-color:color-mix(in srgb,var(--_et-qdt-loading) 50%,transparent)}.et-query-devtools-chip--error{color:var(--_et-qdt-error);border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-chip--success{color:var(--_et-qdt-success);border-color:color-mix(in srgb,var(--_et-qdt-success) 50%,transparent)}button.et-query-devtools-chip--button{appearance:none;cursor:pointer;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}button.et-query-devtools-chip--button:hover{border-color:var(--_et-qdt-accent);color:var(--_et-qdt-accent)}.et-query-devtools-caret{display:inline-block;width:1.2rem;color:var(--_et-qdt-muted)}.et-query-devtools-facet{appearance:none;cursor:pointer;color:var(--_et-qdt-muted);font:inherit;font-size:1.1rem;line-height:1.4}.et-query-devtools-facet:hover:not(:disabled){color:var(--_et-qdt-fg);border-color:color-mix(in srgb,var(--_et-qdt-accent) 60%,transparent)}.et-query-devtools-facet:disabled{color:var(--_et-qdt-subtle);cursor:default}.et-query-devtools-facet--active{color:var(--_et-qdt-accent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-accent) 14%,transparent)}.et-query-devtools-facet-count{font-variant-numeric:tabular-nums;color:inherit;opacity:.75}.et-query-devtools-card{border:1px solid var(--_et-qdt-border);border-radius:.6rem;padding:1rem}.et-query-devtools-card-head{display:flex;align-items:center;gap:.6rem;margin-block-end:.8rem}.et-query-devtools-card-title{display:flex;align-items:baseline;gap:.5rem;margin-block-end:.2rem}.et-query-devtools-card-sub{color:var(--_et-qdt-subtle);font-size:1.1rem;margin-block-end:.6rem;overflow-wrap:anywhere}.et-query-devtools-inner-queries{display:flex;flex-direction:column;gap:.3rem}.et-query-devtools-inner-row{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:1px solid var(--_et-qdt-border);border-radius:.4rem;background:transparent;color:var(--_et-qdt-fg);padding:.4rem .6rem;cursor:pointer;font:inherit}.et-query-devtools-inner-row:hover:not([disabled]){background:var(--_et-qdt-hover)}.et-query-devtools-inner-row--active{background:var(--_et-qdt-active);border-color:var(--_et-qdt-accent)}.et-query-devtools-inner-row[disabled]{cursor:default;opacity:.6}.et-query-devtools-step-list{display:flex;flex-direction:column;gap:.4rem;margin:0;padding:0;list-style:none}.et-query-devtools-step-item{border:1px solid var(--_et-qdt-border);border-radius:.4rem;overflow:hidden}.et-query-devtools-step-item[data-status=error]{border-color:color-mix(in srgb,var(--_et-qdt-error) 50%,transparent)}.et-query-devtools-step-head{display:flex;align-items:stretch}.et-query-devtools-step-head .et-query-devtools-inner-row{border:none;border-radius:0;flex:1 1 auto}.et-query-devtools-step-caret-btn{appearance:none;border:none;border-inline-start:1px solid var(--_et-qdt-border);background:transparent;color:var(--_et-qdt-muted);cursor:pointer;padding:0 .7rem;font:inherit}.et-query-devtools-step-caret-btn:hover{background:var(--_et-qdt-hover);color:var(--_et-qdt-fg)}.et-query-devtools-step-toggle{display:flex;align-items:center;gap:.6rem;inline-size:100%;text-align:start;appearance:none;border:none;background:transparent;color:var(--_et-qdt-fg);padding:.6rem;cursor:pointer;font:inherit}.et-query-devtools-step-toggle:hover{background:var(--_et-qdt-hover)}.et-query-devtools-step-index{flex:1 1 auto;min-inline-size:0}.et-query-devtools-step-caret{color:var(--_et-qdt-muted)}.et-query-devtools-step-io{padding:0 .8rem .8rem;border-block-start:1px solid var(--_et-qdt-border)}.et-query-devtools-table{inline-size:100%;border-collapse:collapse}.et-query-devtools-table th,.et-query-devtools-table td{text-align:start;padding:.5rem .6rem;border-block-end:1px solid var(--_et-qdt-border);vertical-align:top}.et-query-devtools-table th{color:var(--_et-qdt-muted);font-weight:600;position:sticky;inset-block-start:0;background:var(--_et-qdt-bg)}.et-query-devtools-table tr[data-status=error] td{color:var(--_et-qdt-error)}.et-query-devtools-empty{color:var(--_et-qdt-subtle);padding:1.2rem;text-align:center}.et-query-devtools-actions-label{color:var(--_et-qdt-subtle);align-self:center;text-transform:uppercase;font-size:1rem;letter-spacing:.05em}.et-query-devtools-actions-label:not(:first-child){margin-inline-start:.7rem}.et-query-devtools-cell-actions{display:flex;gap:.4rem;white-space:nowrap}.et-query-devtools-section-row{display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-block-start:1rem}.et-query-devtools-search{flex:0 1 20rem;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-row-link{display:inline-flex;align-items:center;gap:.6rem;appearance:none;border:0;background:none;padding:0;color:inherit;font:inherit;text-align:start;cursor:pointer}.et-query-devtools-row-link:hover .et-query-devtools-url,.et-query-devtools-row-link:focus-visible .et-query-devtools-url{color:var(--_et-qdt-accent);text-decoration:underline}.et-query-devtools-hint{flex:1 1 24rem;color:var(--_et-qdt-muted);font-size:1.1rem;margin:0}.et-query-devtools-card--armed{border-color:color-mix(in srgb,var(--_et-qdt-error) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-error) 6%,transparent)}.et-query-devtools-faults{display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));gap:.8rem 1.6rem}.et-query-devtools-faults .et-query-devtools-field{display:grid;grid-template-columns:8rem minmax(0,1fr);align-items:baseline;row-gap:.2rem;color:var(--_et-qdt-fg)}.et-query-devtools-faults .et-query-devtools-field small{grid-column:2;color:var(--_et-qdt-subtle);font-size:1.1rem}.et-query-devtools-input{inline-size:100%;background:var(--_et-qdt-bg);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.4rem .6rem;font:inherit}.et-query-devtools-input--number{inline-size:8rem;font-variant-numeric:tabular-nums}.et-query-devtools-editor{border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.8rem;margin-block-end:1rem}.et-query-devtools-textarea{inline-size:100%;min-block-size:12rem;resize:vertical;background:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);color:var(--_et-qdt-fg);border:1px solid var(--_et-qdt-border);border-radius:.4rem;padding:.6rem;font-family:inherit;font-size:1.2rem}.et-query-devtools-editor-error{color:var(--_et-qdt-error);margin:.4rem 0 0}.et-query-devtools-json-node,.et-query-devtools-json-children{display:flex;flex-direction:column}.et-query-devtools-json-children{padding-inline-start:1.4rem;border-inline-start:1px solid var(--_et-qdt-border);margin-inline-start:.4rem}.et-query-devtools-json-row{display:flex;align-items:baseline;gap:.4rem;padding:.1rem 0;font-family:inherit;line-height:1.5}.et-query-devtools-json-toggle{appearance:none;display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;border:none;background:transparent;color:var(--_et-qdt-fg);cursor:pointer;text-align:start;font:inherit;padding:.1rem 0}.et-query-devtools-json-node>.et-query-devtools-json-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-json-caret{color:var(--_et-qdt-subtle);inline-size:1rem;flex:0 0 auto}.et-query-devtools-json-key{color:var(--_et-qdt-accent)}.et-query-devtools-json-colon,.et-query-devtools-json-preview{color:var(--_et-qdt-subtle)}.et-query-devtools-json-chunk{color:var(--_et-qdt-fg);opacity:.75;font-variant-numeric:tabular-nums}.et-query-devtools-json-value{overflow-wrap:anywhere}.et-query-devtools-json-leaf[data-kind=string] .et-query-devtools-json-value{color:var(--_et-qdt-success)}.et-query-devtools-json-leaf[data-kind=number] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=boolean] .et-query-devtools-json-value{color:var(--_et-qdt-loading)}.et-query-devtools-json-leaf[data-kind=null] .et-query-devtools-json-value,.et-query-devtools-json-leaf[data-kind=undefined] .et-query-devtools-json-value{color:var(--_et-qdt-subtle)}.et-query-devtools-json-hit{background:color-mix(in srgb,var(--_et-qdt-loading) 40%,transparent);border-radius:.2rem}.et-query-devtools-json-copy{appearance:none;display:inline-flex;align-items:center;justify-content:center;align-self:center;flex:0 0 auto;inline-size:2.2rem;block-size:2.2rem;margin-block:-.4rem;border:none;border-radius:.3rem;background:transparent;color:var(--_et-qdt-subtle);cursor:pointer;opacity:0;font:inherit;font-size:1.3rem;line-height:1}.et-query-devtools-json-copy:hover:not(.et-query-devtools-json-copy--copied){background:var(--_et-qdt-active);color:var(--_et-qdt-fg)}.et-query-devtools-json-row:hover .et-query-devtools-json-copy,.et-query-devtools-json-copy:focus-visible,.et-query-devtools-json-copy--copied{opacity:1}.et-query-devtools-json-copy--copied{color:var(--_et-qdt-success)}.et-query-devtools-timeline{display:grid;grid-template-columns:minmax(10rem,26rem) minmax(14rem,1fr) max-content max-content max-content;gap:.1rem .8rem;padding-inline:.4rem 1rem}.et-query-devtools-timeline-axis,.et-query-devtools-timeline-row{display:grid;grid-column:1 / -1;grid-template-columns:subgrid;align-items:center}.et-query-devtools-timeline-axis{position:sticky;inset-block-start:0;z-index:1;background:var(--_et-qdt-bg);border-block-end:1px solid var(--_et-qdt-border);padding-block-end:.4rem;margin-block-end:.4rem}.et-query-devtools-timeline-ticks{grid-column:2;position:relative;block-size:1.5rem}.et-query-devtools-timeline-tick{position:absolute;inset-block-start:0;transform:translate(-50%);color:var(--_et-qdt-subtle);font-size:1.1rem;white-space:nowrap}.et-query-devtools-timeline-tick:first-child{transform:none}.et-query-devtools-timeline-tick:last-child{transform:translate(-100%)}.et-query-devtools-timeline-row{appearance:none;border:0;background:transparent;color:inherit;font:inherit;text-align:start;padding:.15rem 0;cursor:pointer}.et-query-devtools-timeline-row:hover{background:var(--_et-qdt-hover)}.et-query-devtools-timeline-row--active{background:var(--_et-qdt-active)}.et-query-devtools-timeline-label{display:flex;align-items:baseline;gap:.4rem;min-inline-size:0;overflow:hidden}.et-query-devtools-timeline-label .et-query-devtools-url{max-inline-size:none;margin-inline-start:0}.et-query-devtools-timeline-track{position:relative;block-size:1.8rem;border-radius:.2rem;overflow:hidden;background-color:color-mix(in srgb,var(--_et-qdt-interaction) 4%,transparent);background-image:repeating-linear-gradient(to right,var(--_et-qdt-border) 0 1px,transparent 1px 25%)}.et-query-devtools-timeline-bar{position:absolute;inset-block:.4rem;min-inline-size:.2rem;border-radius:.2rem;background:var(--_et-qdt-subtle)}.et-query-devtools-timeline-bar[data-status=success]{background:var(--_et-qdt-success)}.et-query-devtools-timeline-bar[data-status=error]{background:var(--_et-qdt-error)}.et-query-devtools-timeline-bar[data-status=loading]{background:var(--_et-qdt-loading)}.et-query-devtools-timeline-bar[data-status=aborted]{min-inline-size:.5rem;background:color-mix(in srgb,var(--_et-qdt-subtle) 20%,transparent);outline:1px solid color-mix(in srgb,var(--_et-qdt-subtle) 70%,transparent);outline-offset:-1px}.et-query-devtools-timeline-duration,.et-query-devtools-timeline-size{text-align:end;color:var(--_et-qdt-muted);font-size:1.1rem;font-variant-numeric:tabular-nums;white-space:nowrap}.et-query-devtools-timeline-duration{color:var(--_et-qdt-fg)}.et-query-devtools-timeline-flags{display:flex;align-items:center;gap:.3rem;white-space:nowrap}.et-query-devtools-timeline-flags .et-query-devtools-chip{padding:0 .4rem;border-color:transparent;border-radius:.3rem;background:color-mix(in srgb,currentcolor 12%,transparent);font-size:1rem;line-height:1.5;font-variant-numeric:tabular-nums}.et-query-devtools-subtabs{display:flex;flex-wrap:wrap;gap:.2rem;border-block-end:1px solid var(--_et-qdt-border);margin-block:.8rem}.et-query-devtools-subtabs+.et-query-devtools-section-row{padding-block-start:0;border-block-start:0}.et-query-devtools-subtab{appearance:none;display:inline-flex;align-items:center;gap:.4rem;border:0;border-block-end:2px solid transparent;background:transparent;color:var(--_et-qdt-muted);font:inherit;font-size:1.15rem;padding:.4rem .6rem;margin-block-end:-1px;cursor:pointer}.et-query-devtools-subtab:hover{color:var(--_et-qdt-fg)}.et-query-devtools-subtab--active{color:var(--_et-qdt-fg);border-block-end-color:var(--_et-qdt-accent)}.et-query-devtools-run-cell{display:flex;align-items:center;gap:.5rem}.et-query-devtools-btn--active{color:var(--_et-qdt-accent);border-color:color-mix(in srgb,var(--_et-qdt-accent) 55%,transparent);background:color-mix(in srgb,var(--_et-qdt-accent) 12%,transparent)}.et-query-devtools-diff td{max-inline-size:28rem;overflow-wrap:anywhere}.et-query-devtools-diff-before{color:var(--_et-qdt-muted)}.et-query-devtools-diff tr[data-kind=removed] .et-query-devtools-diff-before{color:var(--_et-qdt-error)}.et-query-devtools-diff tr[data-kind=added] .et-query-devtools-diff-after{color:var(--_et-qdt-success)}.et-query-devtools-diff tr[data-kind=changed] .et-query-devtools-diff-after{color:var(--_et-qdt-accent)}}\n"] }]
|
|
48135
49337
|
}], ctorParameters: () => [] });
|
|
48136
49338
|
|
|
48137
49339
|
const QUERY_DEVTOOLS_IMPORTS = [QueryDevtoolsComponent];
|