@camstack/ui-library 1.2.304 → 1.2.305
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/dist/composites/log-history-paging.d.ts +62 -0
- package/dist/composites/log-stream.d.ts +6 -10
- package/dist/generated/system-hooks.d.ts +2 -0
- package/dist/hooks/use-log-history.d.ts +26 -0
- package/dist/index.cjs +337 -77
- package/dist/index.js +337 -78
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure paging logic for the server-served log history (D898).
|
|
3
|
+
*
|
|
4
|
+
* The server answers one TIME-BOUNDED slice per call: `done: false` with fewer
|
|
5
|
+
* than `limit` entries means "the budget ran out, still searching". The client
|
|
6
|
+
* keeps calling with `before: nextBefore` until it holds a full page, the
|
|
7
|
+
* source says `done`, or the operator cancels. Kept free of React and tRPC so
|
|
8
|
+
* the loop is unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
export interface LogWireEntry {
|
|
11
|
+
readonly id?: string;
|
|
12
|
+
readonly timestamp: string;
|
|
13
|
+
readonly level: string;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly scope?: string;
|
|
16
|
+
readonly tags?: Record<string, string | undefined>;
|
|
17
|
+
readonly meta?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface LogWirePage {
|
|
20
|
+
/** NEWEST FIRST. */
|
|
21
|
+
readonly entries: readonly LogWireEntry[];
|
|
22
|
+
readonly nextBefore: string | null;
|
|
23
|
+
readonly nextAfter: string | null;
|
|
24
|
+
readonly scannedBytes: number;
|
|
25
|
+
readonly done: boolean;
|
|
26
|
+
readonly cancelled?: boolean;
|
|
27
|
+
readonly source: string;
|
|
28
|
+
}
|
|
29
|
+
/** Fetches one slice older than `before` (undefined = the first page). */
|
|
30
|
+
export type FetchLogSlice = (before: string | undefined, remaining: number, signal: AbortSignal) => Promise<LogWirePage>;
|
|
31
|
+
export interface CollectedPage {
|
|
32
|
+
/** NEWEST FIRST, across every slice the loop made. */
|
|
33
|
+
readonly entries: readonly LogWireEntry[];
|
|
34
|
+
/** Cursor for the next older page; null when nothing older exists. */
|
|
35
|
+
readonly nextBefore: string | null;
|
|
36
|
+
readonly done: boolean;
|
|
37
|
+
readonly cancelled: boolean;
|
|
38
|
+
readonly scannedBytes: number;
|
|
39
|
+
readonly source: string | undefined;
|
|
40
|
+
}
|
|
41
|
+
export interface CollectPageOptions {
|
|
42
|
+
readonly fetchSlice: FetchLogSlice;
|
|
43
|
+
readonly limit: number;
|
|
44
|
+
readonly before?: string;
|
|
45
|
+
readonly signal: AbortSignal;
|
|
46
|
+
/** Called after every slice with the bytes scanned so far. */
|
|
47
|
+
readonly onProgress?: (scannedBytes: number) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Accumulate slices until `limit` entries, `done`, or an abort. An abort keeps
|
|
51
|
+
* what was already collected and the cursor reached, so "Load older" resumes
|
|
52
|
+
* where the scan stopped instead of starting again.
|
|
53
|
+
*/
|
|
54
|
+
export declare function collectPage(options: CollectPageOptions): Promise<CollectedPage>;
|
|
55
|
+
/** Stable identity of an entry: the server id, else the legacy composite key. */
|
|
56
|
+
export declare function logEntryKey(entry: LogWireEntry): string;
|
|
57
|
+
/**
|
|
58
|
+
* History (newest first, as paged) merged with live entries (arrival order),
|
|
59
|
+
* deduped by key — a live entry wins (richer tags/meta) — and returned
|
|
60
|
+
* chronologically, newest LAST. Ties keep insertion order.
|
|
61
|
+
*/
|
|
62
|
+
export declare function mergeLogEntries(history: readonly LogWireEntry[], live: readonly LogWireEntry[]): LogWireEntry[];
|
|
@@ -10,14 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { type JSX } from "react";
|
|
12
12
|
import { type LiveBuffer } from "../hooks/use-live-buffer";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
readonly level: string;
|
|
16
|
-
readonly message: string;
|
|
17
|
-
readonly scope?: string;
|
|
18
|
-
readonly tags?: Record<string, string | undefined>;
|
|
19
|
-
readonly meta?: Record<string, unknown>;
|
|
20
|
-
}
|
|
13
|
+
import { type LogWireEntry } from "./log-history-paging";
|
|
14
|
+
type LogEntry = LogWireEntry;
|
|
21
15
|
export interface LogStreamProps {
|
|
22
16
|
/** Lock to a specific agent. When set, the agent selector is hidden. */
|
|
23
17
|
readonly agentId?: string;
|
|
@@ -65,7 +59,9 @@ export interface LogStreamProps {
|
|
|
65
59
|
readonly showDeviceChip?: boolean;
|
|
66
60
|
/** Show the filter bar with selectors. Set false for compact inline use. */
|
|
67
61
|
readonly showFilters?: boolean;
|
|
68
|
-
/**
|
|
62
|
+
/** Show the history source selector (default: follows `showFilters`). */
|
|
63
|
+
readonly showSourceSelect?: boolean;
|
|
64
|
+
/** Entries per history page (default: 100). */
|
|
69
65
|
readonly limit?: number;
|
|
70
66
|
/**
|
|
71
67
|
* Optional controlled live buffer. When provided, the stream reads
|
|
@@ -79,7 +75,7 @@ export interface LogStreamProps {
|
|
|
79
75
|
readonly onClose?: () => void;
|
|
80
76
|
readonly className?: string;
|
|
81
77
|
}
|
|
82
|
-
export declare function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight, showScope, showDeviceChip, showFilters, limit, liveBuffer: externalBuffer, onClose, className }: LogStreamProps): JSX.Element;
|
|
78
|
+
export declare function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight, showScope, showDeviceChip, showFilters, showSourceSelect, limit, liveBuffer: externalBuffer, onClose, className }: LogStreamProps): JSX.Element;
|
|
83
79
|
export interface FloatingLogStreamProps extends LogStreamProps {
|
|
84
80
|
/** Position: 'bottom' anchors to parent bottom, 'overlay' is absolute positioned. */
|
|
85
81
|
readonly position?: "bottom" | "overlay";
|
|
@@ -487,6 +487,8 @@ export declare const useDeviceHistoryListNodeSeries: typeof trpc.deviceHistory.l
|
|
|
487
487
|
export declare const useDeviceHistoryQuery: typeof trpc.deviceHistory.query.useQuery;
|
|
488
488
|
/** Generated alias around `trpc.deviceHistory.queryNode.useQuery`. */
|
|
489
489
|
export declare const useDeviceHistoryQueryNode: typeof trpc.deviceHistory.queryNode.useQuery;
|
|
490
|
+
/** Generated alias around `trpc.deviceHistory.queryMany.useQuery`. */
|
|
491
|
+
export declare const useDeviceHistoryQueryMany: typeof trpc.deviceHistory.queryMany.useQuery;
|
|
490
492
|
/** Generated alias around `trpc.deviceHistory.declareSeries.useMutation`. */
|
|
491
493
|
export declare const useDeviceHistoryDeclareSeries: typeof trpc.deviceHistory.declareSeries.useMutation;
|
|
492
494
|
/** Generated alias around `trpc.deviceHistory.record.useMutation`. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type LogWireEntry } from "../composites/log-history-paging";
|
|
2
|
+
export type LogHistoryLevel = "debug" | "info" | "warn" | "error";
|
|
3
|
+
/** The filters the SERVER applies; everything but paging. */
|
|
4
|
+
export interface LogHistoryQuery {
|
|
5
|
+
/** A log-destination addonId or `ring`; undefined = the server default. */
|
|
6
|
+
readonly source?: string;
|
|
7
|
+
readonly level: LogHistoryLevel;
|
|
8
|
+
readonly text?: string;
|
|
9
|
+
readonly tags?: Record<string, string | number>;
|
|
10
|
+
}
|
|
11
|
+
export type LogHistoryStatus = "idle" | "loading";
|
|
12
|
+
export interface LogHistory {
|
|
13
|
+
/** Newest first, as paged. */
|
|
14
|
+
readonly entries: readonly LogWireEntry[];
|
|
15
|
+
readonly status: LogHistoryStatus;
|
|
16
|
+
/** True when nothing older exists ("Beginning of logs"). */
|
|
17
|
+
readonly done: boolean;
|
|
18
|
+
/** Bytes scanned by the scan in flight (0 until its first slice answers). */
|
|
19
|
+
readonly scannedBytes: number;
|
|
20
|
+
/** The source that answered (the server may fall back to `ring`). */
|
|
21
|
+
readonly source: string | undefined;
|
|
22
|
+
readonly error: string | null;
|
|
23
|
+
readonly loadOlder: () => void;
|
|
24
|
+
readonly cancel: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function useLogHistory(query: LogHistoryQuery, limit: number): LogHistory;
|
package/dist/index.cjs
CHANGED
|
@@ -10754,6 +10754,8 @@ var useDeviceHistoryListNodeSeries = trpc.deviceHistory.listNodeSeries.useQuery;
|
|
|
10754
10754
|
var useDeviceHistoryQuery = trpc.deviceHistory.query.useQuery;
|
|
10755
10755
|
/** Generated alias around `trpc.deviceHistory.queryNode.useQuery`. */
|
|
10756
10756
|
var useDeviceHistoryQueryNode = trpc.deviceHistory.queryNode.useQuery;
|
|
10757
|
+
/** Generated alias around `trpc.deviceHistory.queryMany.useQuery`. */
|
|
10758
|
+
var useDeviceHistoryQueryMany = trpc.deviceHistory.queryMany.useQuery;
|
|
10757
10759
|
/** Generated alias around `trpc.deviceHistory.declareSeries.useMutation`. */
|
|
10758
10760
|
var useDeviceHistoryDeclareSeries = trpc.deviceHistory.declareSeries.useMutation;
|
|
10759
10761
|
/** Generated alias around `trpc.deviceHistory.record.useMutation`. */
|
|
@@ -39832,6 +39834,202 @@ function useLiveBuffer(max) {
|
|
|
39832
39834
|
reset
|
|
39833
39835
|
]);
|
|
39834
39836
|
}
|
|
39837
|
+
//#endregion
|
|
39838
|
+
//#region src/hooks/use-debounced-string.ts
|
|
39839
|
+
/**
|
|
39840
|
+
* useDebouncedString — return `value` after `delay` ms have passed
|
|
39841
|
+
* without further changes. Resets the timer on every change.
|
|
39842
|
+
*
|
|
39843
|
+
* Useful for search inputs that should NOT push to URL state on every
|
|
39844
|
+
* keystroke. The DOM input stays controlled by an external `value`
|
|
39845
|
+
* (caller's `useState`); this hook produces a debounced derivation.
|
|
39846
|
+
*/
|
|
39847
|
+
function useDebouncedString(value, delayMs) {
|
|
39848
|
+
const [debounced, setDebounced] = (0, react$1.useState)(value);
|
|
39849
|
+
(0, react$1.useEffect)(() => {
|
|
39850
|
+
const t = setTimeout(() => setDebounced(value), delayMs);
|
|
39851
|
+
return () => clearTimeout(t);
|
|
39852
|
+
}, [value, delayMs]);
|
|
39853
|
+
return debounced;
|
|
39854
|
+
}
|
|
39855
|
+
//#endregion
|
|
39856
|
+
//#region src/composites/log-history-paging.ts
|
|
39857
|
+
/**
|
|
39858
|
+
* Accumulate slices until `limit` entries, `done`, or an abort. An abort keeps
|
|
39859
|
+
* what was already collected and the cursor reached, so "Load older" resumes
|
|
39860
|
+
* where the scan stopped instead of starting again.
|
|
39861
|
+
*/
|
|
39862
|
+
async function collectPage(options) {
|
|
39863
|
+
const { fetchSlice, limit, signal, onProgress } = options;
|
|
39864
|
+
const entries = [];
|
|
39865
|
+
let cursor = options.before;
|
|
39866
|
+
let scannedBytes = 0;
|
|
39867
|
+
let source;
|
|
39868
|
+
for (;;) {
|
|
39869
|
+
if (signal.aborted) return {
|
|
39870
|
+
entries,
|
|
39871
|
+
nextBefore: cursor ?? null,
|
|
39872
|
+
done: false,
|
|
39873
|
+
cancelled: true,
|
|
39874
|
+
scannedBytes,
|
|
39875
|
+
source
|
|
39876
|
+
};
|
|
39877
|
+
let page;
|
|
39878
|
+
try {
|
|
39879
|
+
page = await fetchSlice(cursor, Math.max(1, limit - entries.length), signal);
|
|
39880
|
+
} catch (error) {
|
|
39881
|
+
if (signal.aborted) return {
|
|
39882
|
+
entries,
|
|
39883
|
+
nextBefore: cursor ?? null,
|
|
39884
|
+
done: false,
|
|
39885
|
+
cancelled: true,
|
|
39886
|
+
scannedBytes,
|
|
39887
|
+
source
|
|
39888
|
+
};
|
|
39889
|
+
throw error;
|
|
39890
|
+
}
|
|
39891
|
+
entries.push(...page.entries);
|
|
39892
|
+
scannedBytes += page.scannedBytes;
|
|
39893
|
+
source = page.source;
|
|
39894
|
+
onProgress?.(scannedBytes);
|
|
39895
|
+
const exhausted = page.done || page.nextBefore === null;
|
|
39896
|
+
if (page.cancelled === true) return {
|
|
39897
|
+
entries,
|
|
39898
|
+
nextBefore: page.nextBefore,
|
|
39899
|
+
done: exhausted,
|
|
39900
|
+
cancelled: true,
|
|
39901
|
+
scannedBytes,
|
|
39902
|
+
source
|
|
39903
|
+
};
|
|
39904
|
+
if (exhausted || entries.length >= limit) return {
|
|
39905
|
+
entries,
|
|
39906
|
+
nextBefore: page.nextBefore,
|
|
39907
|
+
done: exhausted,
|
|
39908
|
+
cancelled: false,
|
|
39909
|
+
scannedBytes,
|
|
39910
|
+
source
|
|
39911
|
+
};
|
|
39912
|
+
cursor = page.nextBefore ?? void 0;
|
|
39913
|
+
}
|
|
39914
|
+
}
|
|
39915
|
+
/** Stable identity of an entry: the server id, else the legacy composite key. */
|
|
39916
|
+
function logEntryKey(entry) {
|
|
39917
|
+
if (entry.id !== void 0 && entry.id !== "") return `id:${entry.id}`;
|
|
39918
|
+
return `${entry.timestamp}|${entry.level}|${entry.scope ?? ""}|${entry.message}`;
|
|
39919
|
+
}
|
|
39920
|
+
/**
|
|
39921
|
+
* History (newest first, as paged) merged with live entries (arrival order),
|
|
39922
|
+
* deduped by key — a live entry wins (richer tags/meta) — and returned
|
|
39923
|
+
* chronologically, newest LAST. Ties keep insertion order.
|
|
39924
|
+
*/
|
|
39925
|
+
function mergeLogEntries(history, live) {
|
|
39926
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
39927
|
+
for (const entry of history) byKey.set(logEntryKey(entry), entry);
|
|
39928
|
+
for (const entry of live) byKey.set(logEntryKey(entry), entry);
|
|
39929
|
+
return [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
39930
|
+
}
|
|
39931
|
+
//#endregion
|
|
39932
|
+
//#region src/hooks/use-log-history.ts
|
|
39933
|
+
/**
|
|
39934
|
+
* useLogHistory — pages the server-served log history (D898).
|
|
39935
|
+
*
|
|
39936
|
+
* The first page and every older page go through `collectPage`, which keeps
|
|
39937
|
+
* calling `logs.query` with `before: nextBefore` while the server answers a
|
|
39938
|
+
* short, not-done slice. Changing the filters aborts whatever is in flight and
|
|
39939
|
+
* restarts from the first page; `cancel()` aborts the scan but keeps what was
|
|
39940
|
+
* found and the cursor reached, so "Load older" resumes from there.
|
|
39941
|
+
*/
|
|
39942
|
+
var INITIAL = {
|
|
39943
|
+
entries: [],
|
|
39944
|
+
nextBefore: null,
|
|
39945
|
+
done: false,
|
|
39946
|
+
status: "loading",
|
|
39947
|
+
scannedBytes: 0,
|
|
39948
|
+
source: void 0,
|
|
39949
|
+
error: null
|
|
39950
|
+
};
|
|
39951
|
+
function useLogHistory(query, limit) {
|
|
39952
|
+
const utils = trpc.useUtils();
|
|
39953
|
+
const [state, setState] = (0, react$1.useState)(INITIAL);
|
|
39954
|
+
const stateRef = (0, react$1.useRef)(state);
|
|
39955
|
+
stateRef.current = state;
|
|
39956
|
+
const queryRef = (0, react$1.useRef)(query);
|
|
39957
|
+
queryRef.current = query;
|
|
39958
|
+
const abortRef = (0, react$1.useRef)(null);
|
|
39959
|
+
const queryKey = JSON.stringify(query);
|
|
39960
|
+
const run = (0, react$1.useCallback)(async (before, reset) => {
|
|
39961
|
+
abortRef.current?.abort();
|
|
39962
|
+
const controller = new AbortController();
|
|
39963
|
+
abortRef.current = controller;
|
|
39964
|
+
setState((prev) => ({
|
|
39965
|
+
...reset ? INITIAL : prev,
|
|
39966
|
+
status: "loading",
|
|
39967
|
+
scannedBytes: 0,
|
|
39968
|
+
error: null
|
|
39969
|
+
}));
|
|
39970
|
+
const fetchSlice = (cursor, remaining, signal) => utils.client.logs.query.query({
|
|
39971
|
+
...queryRef.current,
|
|
39972
|
+
limit: remaining,
|
|
39973
|
+
...cursor !== void 0 ? { before: cursor } : {}
|
|
39974
|
+
}, { signal });
|
|
39975
|
+
try {
|
|
39976
|
+
const out = await collectPage({
|
|
39977
|
+
fetchSlice,
|
|
39978
|
+
limit,
|
|
39979
|
+
...before !== void 0 ? { before } : {},
|
|
39980
|
+
signal: controller.signal,
|
|
39981
|
+
onProgress: (scannedBytes) => {
|
|
39982
|
+
if (abortRef.current === controller) setState((prev) => ({
|
|
39983
|
+
...prev,
|
|
39984
|
+
scannedBytes
|
|
39985
|
+
}));
|
|
39986
|
+
}
|
|
39987
|
+
});
|
|
39988
|
+
if (abortRef.current !== controller) return;
|
|
39989
|
+
setState((prev) => ({
|
|
39990
|
+
entries: reset ? out.entries : [...prev.entries, ...out.entries],
|
|
39991
|
+
nextBefore: out.nextBefore,
|
|
39992
|
+
done: out.done,
|
|
39993
|
+
status: "idle",
|
|
39994
|
+
scannedBytes: out.scannedBytes,
|
|
39995
|
+
source: out.source ?? prev.source,
|
|
39996
|
+
error: null
|
|
39997
|
+
}));
|
|
39998
|
+
} catch (error) {
|
|
39999
|
+
if (abortRef.current !== controller) return;
|
|
40000
|
+
setState((prev) => ({
|
|
40001
|
+
...prev,
|
|
40002
|
+
status: "idle",
|
|
40003
|
+
error: error instanceof Error ? error.message : String(error)
|
|
40004
|
+
}));
|
|
40005
|
+
}
|
|
40006
|
+
}, [utils, limit]);
|
|
40007
|
+
(0, react$1.useEffect)(() => {
|
|
40008
|
+
run(void 0, true);
|
|
40009
|
+
return () => {
|
|
40010
|
+
abortRef.current?.abort();
|
|
40011
|
+
abortRef.current = null;
|
|
40012
|
+
};
|
|
40013
|
+
}, [run, queryKey]);
|
|
40014
|
+
const loadOlder = (0, react$1.useCallback)(() => {
|
|
40015
|
+
const current = stateRef.current;
|
|
40016
|
+
if (current.status === "loading" || current.done) return;
|
|
40017
|
+
run(current.nextBefore ?? void 0, false);
|
|
40018
|
+
}, [run]);
|
|
40019
|
+
const cancel = (0, react$1.useCallback)(() => {
|
|
40020
|
+
abortRef.current?.abort();
|
|
40021
|
+
}, []);
|
|
40022
|
+
return {
|
|
40023
|
+
entries: state.entries,
|
|
40024
|
+
status: state.status,
|
|
40025
|
+
done: state.done,
|
|
40026
|
+
scannedBytes: state.scannedBytes,
|
|
40027
|
+
source: state.source,
|
|
40028
|
+
error: state.error,
|
|
40029
|
+
loadOlder,
|
|
40030
|
+
cancel
|
|
40031
|
+
};
|
|
40032
|
+
}
|
|
39835
40033
|
/**
|
|
39836
40034
|
* True when the viewport is scrolled to (or within `threshold` px of) the
|
|
39837
40035
|
* bottom of its content.
|
|
@@ -39888,12 +40086,6 @@ var LEVEL_STYLES = {
|
|
|
39888
40086
|
color: "text-gray-500"
|
|
39889
40087
|
}
|
|
39890
40088
|
};
|
|
39891
|
-
var LEVEL_SEVERITY = {
|
|
39892
|
-
debug: 0,
|
|
39893
|
-
info: 1,
|
|
39894
|
-
warn: 2,
|
|
39895
|
-
error: 3
|
|
39896
|
-
};
|
|
39897
40089
|
var LEVEL_OPTIONS = [
|
|
39898
40090
|
"debug",
|
|
39899
40091
|
"info",
|
|
@@ -39902,15 +40094,18 @@ var LEVEL_OPTIONS = [
|
|
|
39902
40094
|
];
|
|
39903
40095
|
/** Max live entries kept in the ring buffer. */
|
|
39904
40096
|
var MAX_LIVE_ENTRIES$1 = 500;
|
|
39905
|
-
|
|
39906
|
-
|
|
39907
|
-
|
|
39908
|
-
|
|
39909
|
-
|
|
40097
|
+
/** Text filter debounce before it goes to the server. */
|
|
40098
|
+
var TEXT_DEBOUNCE_MS = 300;
|
|
40099
|
+
/** Distance from the top (px) that asks for the next older page. */
|
|
40100
|
+
var TOP_LOAD_THRESHOLD_PX = 24;
|
|
40101
|
+
var BYTES_PER_MB = 1024 * 1024;
|
|
40102
|
+
function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: propsDeviceId, containerDeviceId: propsContainerDeviceId, integrationId: propsIntegrationId, requestId: propsRequestId, level: initialLevel, maxHeight = "max-h-96", showScope = false, showDeviceChip = false, showFilters = true, showSourceSelect, limit = 100, liveBuffer: externalBuffer, onClose, className }) {
|
|
39910
40103
|
const t = useUiText();
|
|
40104
|
+
const sourceSelectOn = showSourceSelect ?? showFilters;
|
|
39911
40105
|
const [localAddonId, setLocalAddonId] = (0, react$1.useState)("");
|
|
39912
40106
|
const [localDeviceId, setLocalDeviceId] = (0, react$1.useState)("");
|
|
39913
40107
|
const [levelFilter, setLevelFilter] = (0, react$1.useState)(initialLevel ?? "info");
|
|
40108
|
+
const [sourceChoice, setSourceChoice] = (0, react$1.useState)(void 0);
|
|
39914
40109
|
const agentId = propsAgentId;
|
|
39915
40110
|
const addonId = propsAddonId ?? (localAddonId || void 0);
|
|
39916
40111
|
const deviceId = propsDeviceId ?? (localDeviceId ? Number(localDeviceId) : void 0);
|
|
@@ -39955,48 +40150,63 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
39955
40150
|
setExpandAll(false);
|
|
39956
40151
|
}
|
|
39957
40152
|
}, [scopeKey, buffer]);
|
|
39958
|
-
const
|
|
39959
|
-
|
|
39960
|
-
|
|
39961
|
-
|
|
39962
|
-
|
|
40153
|
+
const debouncedText = useDebouncedString(searchText, TEXT_DEBOUNCE_MS).trim();
|
|
40154
|
+
const sourcesQuery = trpc.logs.sources.useQuery(void 0, { staleTime: 6e4 });
|
|
40155
|
+
const readableSources = (0, react$1.useMemo)(() => (sourcesQuery.data ?? []).filter((source) => source.readable), [sourcesQuery.data]);
|
|
40156
|
+
const history = useLogHistory((0, react$1.useMemo)(() => ({
|
|
40157
|
+
...sourceChoice ? { source: sourceChoice } : {},
|
|
40158
|
+
level: levelFilter,
|
|
40159
|
+
...debouncedText ? { text: debouncedText } : {},
|
|
40160
|
+
...tags ? { tags } : {}
|
|
40161
|
+
}), [
|
|
40162
|
+
sourceChoice,
|
|
40163
|
+
levelFilter,
|
|
40164
|
+
debouncedText,
|
|
40165
|
+
tags
|
|
40166
|
+
]), limit);
|
|
40167
|
+
trpc.logs.subscribe.useSubscription({
|
|
40168
|
+
level: levelFilter,
|
|
40169
|
+
...debouncedText ? { text: debouncedText } : {},
|
|
40170
|
+
...tags ? { tags } : {}
|
|
40171
|
+
}, { onData: (entry) => {
|
|
39963
40172
|
buffer.append(entry);
|
|
39964
40173
|
} });
|
|
40174
|
+
const filterKey = `${sourceChoice ?? ""}|${levelFilter}|${debouncedText}`;
|
|
40175
|
+
const prevFilterRef = (0, react$1.useRef)(filterKey);
|
|
40176
|
+
(0, react$1.useEffect)(() => {
|
|
40177
|
+
if (prevFilterRef.current === filterKey) return;
|
|
40178
|
+
prevFilterRef.current = filterKey;
|
|
40179
|
+
buffer.reset();
|
|
40180
|
+
setFollow(true);
|
|
40181
|
+
}, [filterKey, buffer]);
|
|
39965
40182
|
const allLogs = (0, react$1.useMemo)(() => {
|
|
39966
|
-
const
|
|
39967
|
-
|
|
39968
|
-
level: log.level,
|
|
39969
|
-
message: log.message,
|
|
39970
|
-
scope: log.scope,
|
|
39971
|
-
tags: log.tags,
|
|
39972
|
-
meta: log.meta
|
|
39973
|
-
}));
|
|
39974
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
39975
|
-
for (const log of initial) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
|
|
39976
|
-
for (const log of liveLogs) byKey.set(`${log.timestamp}|${log.level}|${log.scope ?? ""}|${log.message}`, log);
|
|
39977
|
-
const merged = [...byKey.values()].toSorted((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
39978
|
-
const levelFiltered = (clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged).filter((log) => passesLevel(log.level, levelFilter));
|
|
39979
|
-
const needle = searchText.trim().toLowerCase();
|
|
39980
|
-
if (!needle) return levelFiltered;
|
|
39981
|
-
return levelFiltered.filter((log) => {
|
|
39982
|
-
if (log.message.toLowerCase().includes(needle)) return true;
|
|
39983
|
-
if (log.scope && log.scope.toLowerCase().includes(needle)) return true;
|
|
39984
|
-
if (log.tags) {
|
|
39985
|
-
for (const v of Object.values(log.tags)) if (v !== void 0 && String(v).toLowerCase().includes(needle)) return true;
|
|
39986
|
-
}
|
|
39987
|
-
if (log.meta) for (const v of Object.values(log.meta)) {
|
|
39988
|
-
if (v === void 0 || v === null) continue;
|
|
39989
|
-
if (formatMetaValue(v).toLowerCase().includes(needle)) return true;
|
|
39990
|
-
}
|
|
39991
|
-
return false;
|
|
39992
|
-
});
|
|
40183
|
+
const merged = mergeLogEntries(history.entries, liveLogs);
|
|
40184
|
+
return clearedAt > 0 ? merged.filter((log) => new Date(log.timestamp).getTime() > clearedAt) : merged;
|
|
39993
40185
|
}, [
|
|
39994
|
-
|
|
40186
|
+
history.entries,
|
|
39995
40187
|
liveLogs,
|
|
39996
|
-
|
|
39997
|
-
clearedAt,
|
|
39998
|
-
searchText
|
|
40188
|
+
clearedAt
|
|
39999
40189
|
]);
|
|
40190
|
+
const prependAnchorRef = (0, react$1.useRef)(null);
|
|
40191
|
+
const requestOlder = (0, react$1.useCallback)(() => {
|
|
40192
|
+
if (history.status === "loading" || history.done) return;
|
|
40193
|
+
const el = scrollRef.current;
|
|
40194
|
+
if (el) prependAnchorRef.current = {
|
|
40195
|
+
height: el.scrollHeight,
|
|
40196
|
+
top: el.scrollTop
|
|
40197
|
+
};
|
|
40198
|
+
history.loadOlder();
|
|
40199
|
+
}, [history]);
|
|
40200
|
+
(0, react$1.useLayoutEffect)(() => {
|
|
40201
|
+
const anchor = prependAnchorRef.current;
|
|
40202
|
+
const el = scrollRef.current;
|
|
40203
|
+
if (!anchor || !el || history.status !== "idle") return;
|
|
40204
|
+
prependAnchorRef.current = null;
|
|
40205
|
+
const next = anchor.top + (el.scrollHeight - anchor.height);
|
|
40206
|
+
if (Math.abs(next - el.scrollTop) < 1) return;
|
|
40207
|
+
programmaticScrollRef.current = true;
|
|
40208
|
+
el.scrollTop = next;
|
|
40209
|
+
}, [history.status, history.entries.length]);
|
|
40000
40210
|
const scrollToBottom = (0, react$1.useCallback)((behavior) => {
|
|
40001
40211
|
const el = scrollRef.current;
|
|
40002
40212
|
if (!el) return;
|
|
@@ -40007,15 +40217,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40007
40217
|
});
|
|
40008
40218
|
}, []);
|
|
40009
40219
|
const [hasNewWhilePaused, setHasNewWhilePaused] = (0, react$1.useState)(false);
|
|
40010
|
-
const
|
|
40220
|
+
const lastKey = allLogs.length > 0 ? logEntryKey(allLogs[allLogs.length - 1]) : "";
|
|
40221
|
+
const prevLastKeyRef = (0, react$1.useRef)(lastKey);
|
|
40011
40222
|
(0, react$1.useLayoutEffect)(() => {
|
|
40012
|
-
const
|
|
40013
|
-
|
|
40014
|
-
if (!
|
|
40223
|
+
const newestChanged = lastKey !== "" && lastKey !== prevLastKeyRef.current;
|
|
40224
|
+
prevLastKeyRef.current = lastKey;
|
|
40225
|
+
if (!newestChanged) return;
|
|
40015
40226
|
if (follow) scrollToBottom();
|
|
40016
40227
|
else setHasNewWhilePaused(true);
|
|
40017
40228
|
}, [
|
|
40018
|
-
|
|
40229
|
+
lastKey,
|
|
40019
40230
|
follow,
|
|
40020
40231
|
scrollToBottom
|
|
40021
40232
|
]);
|
|
@@ -40035,8 +40246,9 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40035
40246
|
if (!el) return;
|
|
40036
40247
|
const pinned = isPinnedToBottom(el, 40);
|
|
40037
40248
|
setFollow((prev) => prev === pinned ? prev : pinned);
|
|
40038
|
-
|
|
40039
|
-
|
|
40249
|
+
if (el.scrollTop <= TOP_LOAD_THRESHOLD_PX) requestOlder();
|
|
40250
|
+
}, [requestOlder]);
|
|
40251
|
+
const expandableKeys = (0, react$1.useMemo)(() => allLogs.filter((log) => log.tags && Object.keys(log.tags).length > 0 || log.meta && Object.keys(log.meta).length > 0).map(logEntryKey), [allLogs]);
|
|
40040
40252
|
const toggleExpandAll = (0, react$1.useCallback)(() => {
|
|
40041
40253
|
setExpandAll((prev) => {
|
|
40042
40254
|
if (prev) setExpandedRows(/* @__PURE__ */ new Set());
|
|
@@ -40083,7 +40295,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40083
40295
|
setTimeout(() => setCopiedRowKey((prev) => prev === key ? null : prev), 1200);
|
|
40084
40296
|
});
|
|
40085
40297
|
}, []);
|
|
40086
|
-
const rowKey =
|
|
40298
|
+
const rowKey = logEntryKey;
|
|
40087
40299
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
40088
40300
|
className: cn("h-full min-h-0 flex flex-col", className),
|
|
40089
40301
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -40118,6 +40330,23 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40118
40330
|
children: [l, "+"]
|
|
40119
40331
|
}, l))
|
|
40120
40332
|
}),
|
|
40333
|
+
sourceSelectOn && readableSources.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
40334
|
+
className: "w-36",
|
|
40335
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Select, {
|
|
40336
|
+
"aria-label": t("uiLibrary.logStream.source", "Log source"),
|
|
40337
|
+
className: "h-5 text-[10px]",
|
|
40338
|
+
value: sourceChoice ?? readableSources.find((x) => x.isDefault)?.addonId ?? "",
|
|
40339
|
+
onChange: (e) => setSourceChoice(e.target.value),
|
|
40340
|
+
options: readableSources.map((x) => ({
|
|
40341
|
+
value: x.addonId,
|
|
40342
|
+
label: x.addonId
|
|
40343
|
+
}))
|
|
40344
|
+
})
|
|
40345
|
+
}),
|
|
40346
|
+
sourceSelectOn && readableSources.length <= 1 && history.source && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
40347
|
+
className: "text-[9px] text-foreground-subtle",
|
|
40348
|
+
children: t("uiLibrary.logStream.sourceName", "Source: {{name}}", { name: history.source })
|
|
40349
|
+
}),
|
|
40121
40350
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolbarButton, {
|
|
40122
40351
|
icon: ArrowDownToLine,
|
|
40123
40352
|
label: follow ? t("uiLibrary.logStream.following", "Following") : t("uiLibrary.logStream.paused", "Paused"),
|
|
@@ -40216,11 +40445,16 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
40216
40445
|
onScroll: handleScroll,
|
|
40217
40446
|
className: cn("flex-1 min-h-0 overflow-auto font-mono text-[10px]", maxHeight),
|
|
40218
40447
|
children: [
|
|
40219
|
-
|
|
40220
|
-
|
|
40221
|
-
|
|
40448
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HistoryEdge, {
|
|
40449
|
+
status: history.status,
|
|
40450
|
+
done: history.done,
|
|
40451
|
+
scannedBytes: history.scannedBytes,
|
|
40452
|
+
hasRows: allLogs.length > 0,
|
|
40453
|
+
error: history.error,
|
|
40454
|
+
onLoadOlder: requestOlder,
|
|
40455
|
+
onCancel: history.cancel
|
|
40222
40456
|
}),
|
|
40223
|
-
|
|
40457
|
+
history.status === "idle" && history.done && allLogs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
40224
40458
|
className: "py-4 text-center text-foreground-subtle",
|
|
40225
40459
|
children: t("uiLibrary.logStream.noLogs", "No logs found")
|
|
40226
40460
|
}),
|
|
@@ -40344,6 +40578,49 @@ function DeviceChip({ deviceId, deviceName }) {
|
|
|
40344
40578
|
children: label
|
|
40345
40579
|
});
|
|
40346
40580
|
}
|
|
40581
|
+
/**
|
|
40582
|
+
* The top of the list: the state of the older-history scan. A scan that is
|
|
40583
|
+
* still looking (short page, source not done) says how much it has read and
|
|
40584
|
+
* can be cancelled; the end of the source is stated quietly.
|
|
40585
|
+
*/
|
|
40586
|
+
function HistoryEdge({ status, done, scannedBytes, hasRows, error, onLoadOlder, onCancel }) {
|
|
40587
|
+
const t = useUiText();
|
|
40588
|
+
if (status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
40589
|
+
className: "flex items-center justify-center gap-2 py-2 text-foreground-subtle",
|
|
40590
|
+
children: [
|
|
40591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }),
|
|
40592
|
+
scannedBytes > 0 ? t("uiLibrary.logStream.searching", "Searching older logs… ({{mb}} MB scanned)", { mb: (scannedBytes / BYTES_PER_MB).toFixed(1) }) : t("uiLibrary.logStream.loading", "Loading logs..."),
|
|
40593
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
40594
|
+
variant: "outline",
|
|
40595
|
+
size: "sm",
|
|
40596
|
+
onClick: onCancel,
|
|
40597
|
+
children: t("uiLibrary.logStream.cancel", "Cancel")
|
|
40598
|
+
})
|
|
40599
|
+
]
|
|
40600
|
+
});
|
|
40601
|
+
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
40602
|
+
className: "flex items-center justify-center gap-2 py-2 text-danger",
|
|
40603
|
+
children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
40604
|
+
variant: "outline",
|
|
40605
|
+
size: "sm",
|
|
40606
|
+
onClick: onLoadOlder,
|
|
40607
|
+
children: t("uiLibrary.logStream.retry", "Retry")
|
|
40608
|
+
})]
|
|
40609
|
+
});
|
|
40610
|
+
if (done) return hasRows ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
40611
|
+
className: "py-2 text-center text-foreground-subtle",
|
|
40612
|
+
children: t("uiLibrary.logStream.beginning", "Beginning of logs")
|
|
40613
|
+
}) : null;
|
|
40614
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
40615
|
+
className: "flex justify-center py-1.5",
|
|
40616
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
40617
|
+
variant: "outline",
|
|
40618
|
+
size: "sm",
|
|
40619
|
+
onClick: onLoadOlder,
|
|
40620
|
+
children: t("uiLibrary.logStream.loadOlder", "Load older")
|
|
40621
|
+
})
|
|
40622
|
+
});
|
|
40623
|
+
}
|
|
40347
40624
|
function ScopeBadge$2({ label, value }) {
|
|
40348
40625
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
40349
40626
|
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
|
|
@@ -52387,24 +52664,6 @@ function DeviceBatchActionsBar(props) {
|
|
|
52387
52664
|
})] });
|
|
52388
52665
|
}
|
|
52389
52666
|
//#endregion
|
|
52390
|
-
//#region src/hooks/use-debounced-string.ts
|
|
52391
|
-
/**
|
|
52392
|
-
* useDebouncedString — return `value` after `delay` ms have passed
|
|
52393
|
-
* without further changes. Resets the timer on every change.
|
|
52394
|
-
*
|
|
52395
|
-
* Useful for search inputs that should NOT push to URL state on every
|
|
52396
|
-
* keystroke. The DOM input stays controlled by an external `value`
|
|
52397
|
-
* (caller's `useState`); this hook produces a debounced derivation.
|
|
52398
|
-
*/
|
|
52399
|
-
function useDebouncedString(value, delayMs) {
|
|
52400
|
-
const [debounced, setDebounced] = (0, react$1.useState)(value);
|
|
52401
|
-
(0, react$1.useEffect)(() => {
|
|
52402
|
-
const t = setTimeout(() => setDebounced(value), delayMs);
|
|
52403
|
-
return () => clearTimeout(t);
|
|
52404
|
-
}, [value, delayMs]);
|
|
52405
|
-
return debounced;
|
|
52406
|
-
}
|
|
52407
|
-
//#endregion
|
|
52408
52667
|
//#region src/hooks/use-persisted-page-size.ts
|
|
52409
52668
|
/**
|
|
52410
52669
|
* usePersistedPageSize — a "rows per page" choice remembered per viewer.
|
|
@@ -65703,6 +65962,7 @@ exports.useDeviceHistoryGetStatus = useDeviceHistoryGetStatus;
|
|
|
65703
65962
|
exports.useDeviceHistoryListNodeSeries = useDeviceHistoryListNodeSeries;
|
|
65704
65963
|
exports.useDeviceHistoryListSeries = useDeviceHistoryListSeries;
|
|
65705
65964
|
exports.useDeviceHistoryQuery = useDeviceHistoryQuery;
|
|
65965
|
+
exports.useDeviceHistoryQueryMany = useDeviceHistoryQueryMany;
|
|
65706
65966
|
exports.useDeviceHistoryQueryNode = useDeviceHistoryQueryNode;
|
|
65707
65967
|
exports.useDeviceHistoryRecord = useDeviceHistoryRecord;
|
|
65708
65968
|
exports.useDeviceHistoryRecordBuckets = useDeviceHistoryRecordBuckets;
|