@reopt-ai/opt-ui 1.11.0 → 1.12.0
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/COMPONENT_CATALOG.md +394 -201
- package/README.md +43 -5
- package/dist/core/index.cjs +2 -2
- package/dist/core/index.d.cts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/docs/02-components/01-core.md +150 -84
- package/dist/docs/02-components/02-visuals.md +1 -1
- package/dist/docs/02-components/03-shells.md +258 -117
- package/dist/docs/02-components/04-surfaces.md +1 -1
- package/dist/docs/02-components/index.md +5 -5
- package/dist/docs/03-recipes/02-dashboards.md +126 -10
- package/dist/{field-sidebar-DAuN4jPV.cjs → field-sidebar-BYgEmdy0.cjs} +1539 -1536
- package/dist/{field-sidebar-BXl5RJ3q.js → field-sidebar-BuO37l4c.js} +1540 -1539
- package/dist/{field-sidebar-B5SQC35J.d.cts → field-sidebar-CrYlRZew.d.ts} +164 -154
- package/dist/{field-sidebar-CBeWE8xX.d.ts → field-sidebar-uXmoyvd7.d.cts} +164 -154
- package/dist/id-registry.cjs +78 -6
- package/dist/id-registry.d.cts +1 -2
- package/dist/id-registry.d.ts +1 -2
- package/dist/id-registry.js +78 -8
- package/dist/id-registry.json +168 -19
- package/dist/index.cjs +1127 -224
- package/dist/index.d.cts +567 -58
- package/dist/index.d.ts +567 -58
- package/dist/index.js +1118 -227
- package/dist/{key-pad-menu-BxTy_u0Q.d.cts → key-pad-menu-CwcY_Vps.d.cts} +2 -3
- package/dist/{key-pad-menu-wXkWn1I1.js → key-pad-menu-Di2aRGbM.js} +4 -6
- package/dist/{key-pad-menu-Cg6rZBie.cjs → key-pad-menu-SItoHPLu.cjs} +1 -1
- package/dist/{key-pad-menu-u5GTJl9B.d.ts → key-pad-menu-WBJShCJn.d.ts} +3 -4
- package/dist/meta.cjs +678 -19
- package/dist/meta.d.cts +12 -36
- package/dist/meta.d.ts +12 -36
- package/dist/meta.js +678 -21
- package/dist/query.d.cts +1 -2
- package/dist/query.d.ts +1 -2
- package/dist/query.js +0 -2
- package/dist/shells/index.cjs +1 -1
- package/dist/shells/index.d.cts +1 -1
- package/dist/shells/index.d.ts +1 -1
- package/dist/shells/index.js +1 -1
- package/dist/{text-truncate-DW78lg4S.d.cts → text-truncate-CQyfUZvq.d.cts} +31 -3
- package/dist/{text-truncate-DW78lg4S.d.ts → text-truncate-CQyfUZvq.d.ts} +31 -3
- package/dist/{text-truncate-C17HHCR-.js → text-truncate-DSuZECy0.js} +43 -10
- package/dist/{text-truncate-BZdL1k5Z.cjs → text-truncate-I1XLWmRa.cjs} +48 -7
- package/dist/theme/server.d.cts +1 -2
- package/dist/theme/server.d.ts +1 -2
- package/dist/theme/server.js +0 -2
- package/dist/visuals/index.js +0 -1
- package/package.json +4 -3
- package/dist/field-sidebar-B5SQC35J.d.cts.map +0 -1
- package/dist/field-sidebar-BXl5RJ3q.js.map +0 -1
- package/dist/field-sidebar-CBeWE8xX.d.ts.map +0 -1
- package/dist/id-registry.d.cts.map +0 -1
- package/dist/id-registry.d.ts.map +0 -1
- package/dist/id-registry.js.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/key-pad-menu-BxTy_u0Q.d.cts.map +0 -1
- package/dist/key-pad-menu-u5GTJl9B.d.ts.map +0 -1
- package/dist/key-pad-menu-wXkWn1I1.js.map +0 -1
- package/dist/meta.d.cts.map +0 -1
- package/dist/meta.d.ts.map +0 -1
- package/dist/meta.js.map +0 -1
- package/dist/query.d.cts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js.map +0 -1
- package/dist/text-truncate-C17HHCR-.js.map +0 -1
- package/dist/text-truncate-DW78lg4S.d.cts.map +0 -1
- package/dist/text-truncate-DW78lg4S.d.ts.map +0 -1
- package/dist/theme/server.d.cts.map +0 -1
- package/dist/theme/server.d.ts.map +0 -1
- package/dist/theme/server.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
const require_text_truncate = require("./text-truncate-
|
|
2
|
+
const require_text_truncate = require("./text-truncate-I1XLWmRa.cjs");
|
|
3
3
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
4
|
let react = require("react");
|
|
5
5
|
react = require_text_truncate.__toESM(react, 1);
|
|
@@ -1475,7 +1475,7 @@ function DataTableProgressBar({ value, barColor, label }) {
|
|
|
1475
1475
|
});
|
|
1476
1476
|
}
|
|
1477
1477
|
/** Renders the data table component. */
|
|
1478
|
-
function DataTable({ data, columns, keyExtractor, title
|
|
1478
|
+
function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick, emptyMessage = "No data available", activeRowId, sortable = false, selectable = false, selectedKeys, onSelectionChange, expandable = false, renderExpanded, pageSize, stickyHeader = false, resizableColumns = false, onSort: onSortExternal, className }) {
|
|
1479
1479
|
const [sortCol, setSortCol] = (0, react.useState)(null);
|
|
1480
1480
|
const [sortDir, setSortDir] = (0, react.useState)(null);
|
|
1481
1481
|
const [expandedKeys, setExpandedKeys] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
@@ -1569,8 +1569,8 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
|
|
|
1569
1569
|
focusWrap: true,
|
|
1570
1570
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_reopt_ai_opt_ui_primitives.Composite, {
|
|
1571
1571
|
role: "grid",
|
|
1572
|
-
"aria-label": title,
|
|
1573
|
-
className: "w-full",
|
|
1572
|
+
"aria-label": title !== null && title !== void 0 ? title : ariaLabel,
|
|
1573
|
+
className: "w-full min-w-max",
|
|
1574
1574
|
render: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("table", {}),
|
|
1575
1575
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", {
|
|
1576
1576
|
className: require_text_truncate.cn("border-border-subtle bg-surface-inset border-b", stickyHeader && "sticky top-0 z-10"),
|
|
@@ -1601,7 +1601,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
|
|
|
1601
1601
|
tabIndex: isSortable ? 0 : void 0,
|
|
1602
1602
|
"aria-sort": sortCol === col.id && sortDir ? sortDir === "asc" ? "ascending" : "descending" : void 0,
|
|
1603
1603
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1604
|
-
className: "flex items-center",
|
|
1604
|
+
className: "flex items-center whitespace-nowrap",
|
|
1605
1605
|
children: [col.header, sortIndicator(col.id)]
|
|
1606
1606
|
}), resizableColumns && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1607
1607
|
role: "separator",
|
|
@@ -1665,7 +1665,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
|
|
|
1665
1665
|
className: require_text_truncate.cn("px-3 py-2.5 text-sm text-text-primary whitespace-nowrap", "w-10"),
|
|
1666
1666
|
role: "gridcell"
|
|
1667
1667
|
}),
|
|
1668
|
-
className: "outline-none data-[
|
|
1668
|
+
className: "outline-none data-[focus-visible]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
|
|
1669
1669
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Checkbox, {
|
|
1670
1670
|
checked: isSelected,
|
|
1671
1671
|
onChange: () => toggleSelectRow(key),
|
|
@@ -1677,7 +1677,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
|
|
|
1677
1677
|
className: require_text_truncate.cn("px-3 py-2.5 text-sm text-text-primary whitespace-nowrap", "w-10"),
|
|
1678
1678
|
role: "gridcell"
|
|
1679
1679
|
}),
|
|
1680
|
-
className: "outline-none data-[
|
|
1680
|
+
className: "outline-none data-[focus-visible]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
|
|
1681
1681
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1682
1682
|
type: "button",
|
|
1683
1683
|
onClick: (e) => {
|
|
@@ -2950,1651 +2950,1654 @@ function Flyout({ open, onClose, title, description, type = "overlay", side = "r
|
|
|
2950
2950
|
}
|
|
2951
2951
|
Flyout.displayName = "Flyout";
|
|
2952
2952
|
//#endregion
|
|
2953
|
-
//#region src/lib/
|
|
2953
|
+
//#region src/lib/query/query-ast.ts
|
|
2954
|
+
const EMPTY_QUERY = { clauses: [] };
|
|
2955
|
+
/** Whether a query constrains anything at all. */
|
|
2956
|
+
function isEmptyQuery(query) {
|
|
2957
|
+
return !query || query.clauses.length === 0;
|
|
2958
|
+
}
|
|
2959
|
+
function sameClause(a, b) {
|
|
2960
|
+
if (a.type !== b.type) return false;
|
|
2961
|
+
if (a.type === "term" && b.type === "term") return a.value === b.value && a.negated === b.negated;
|
|
2962
|
+
const fieldA = a;
|
|
2963
|
+
const fieldB = b;
|
|
2964
|
+
return fieldA.field === fieldB.field && fieldA.operator === fieldB.operator && fieldA.negated === fieldB.negated && fieldA.values.length === fieldB.values.length && fieldA.values.every((value, index) => value === fieldB.values[index]);
|
|
2965
|
+
}
|
|
2966
|
+
/** Add a clause unless an identical one is already there. */
|
|
2967
|
+
function addClause(query, clause) {
|
|
2968
|
+
if (query.clauses.some((existing) => sameClause(existing, clause))) return query;
|
|
2969
|
+
return { clauses: [...query.clauses, clause] };
|
|
2970
|
+
}
|
|
2954
2971
|
/**
|
|
2955
|
-
*
|
|
2972
|
+
* Constrain a field to a value — what a cell's "filter for this" action does.
|
|
2956
2973
|
*
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
* promise keeps exactly one refresh in flight, and a rejection stops the loop
|
|
2961
|
-
* instead of hammering an endpoint that is already failing.
|
|
2974
|
+
* Applying the opposite of a constraint already present replaces it rather than
|
|
2975
|
+
* adding it: `status:200` then "filter out 200" should read as one changed
|
|
2976
|
+
* mind, not as a query that can never match.
|
|
2962
2977
|
*/
|
|
2963
|
-
function
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
(
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
var _callbackRef$current;
|
|
2975
|
-
await ((_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef));
|
|
2976
|
-
} catch (_unused) {
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
if (!cancelled) schedule();
|
|
2980
|
-
}, interval);
|
|
2981
|
-
};
|
|
2982
|
-
schedule();
|
|
2983
|
-
return () => {
|
|
2984
|
-
cancelled = true;
|
|
2985
|
-
if (timer) clearTimeout(timer);
|
|
2986
|
-
};
|
|
2987
|
-
}, [interval, paused]);
|
|
2978
|
+
function withFieldValue(query, field, value, options = {}) {
|
|
2979
|
+
var _options$negated, _options$operator;
|
|
2980
|
+
const negated = (_options$negated = options.negated) !== null && _options$negated !== void 0 ? _options$negated : false;
|
|
2981
|
+
const operator = (_options$operator = options.operator) !== null && _options$operator !== void 0 ? _options$operator : "eq";
|
|
2982
|
+
return { clauses: [...query.clauses.filter((clause) => !(clause.type === "field" && clause.field === field && clause.operator === operator && clause.values.length === 1 && clause.values[0] === value)), {
|
|
2983
|
+
type: "field",
|
|
2984
|
+
field,
|
|
2985
|
+
operator,
|
|
2986
|
+
values: [value],
|
|
2987
|
+
negated
|
|
2988
|
+
}] };
|
|
2988
2989
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
autoRefreshOff: "Off",
|
|
3007
|
-
every: (duration) => `Every ${duration}`,
|
|
3008
|
-
previous: "Previous window",
|
|
3009
|
-
nextWindow: "Next window",
|
|
3010
|
-
zoomIn: "Zoom in",
|
|
3011
|
-
zoomOut: "Zoom out",
|
|
3012
|
-
thisUnit: (unit) => `This ${unit}`,
|
|
3013
|
-
units: {
|
|
3014
|
-
s: "seconds",
|
|
3015
|
-
m: "minutes",
|
|
3016
|
-
h: "hours",
|
|
3017
|
-
d: "days",
|
|
3018
|
-
w: "weeks",
|
|
3019
|
-
M: "months",
|
|
3020
|
-
y: "years"
|
|
3021
|
-
}
|
|
3022
|
-
};
|
|
3023
|
-
const QUICK_UNITS = [
|
|
3024
|
-
"m",
|
|
3025
|
-
"h",
|
|
3026
|
-
"d",
|
|
3027
|
-
"w",
|
|
3028
|
-
"M",
|
|
3029
|
-
"y"
|
|
3030
|
-
];
|
|
3031
|
-
const REFRESH_INTERVALS = [
|
|
3032
|
-
0,
|
|
3033
|
-
5e3,
|
|
3034
|
-
1e4,
|
|
3035
|
-
3e4,
|
|
3036
|
-
6e4,
|
|
3037
|
-
3e5
|
|
3038
|
-
];
|
|
3039
|
-
function selectValue(value) {
|
|
3040
|
-
var _value$;
|
|
3041
|
-
return Array.isArray(value) ? (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : "" : value;
|
|
2990
|
+
/** Drop every clause naming `field`. */
|
|
2991
|
+
function withoutField(query, field) {
|
|
2992
|
+
return { clauses: query.clauses.filter((clause) => clause.type !== "field" || clause.field !== field) };
|
|
2993
|
+
}
|
|
2994
|
+
/** Drop the clause at `index`, for a removable chip. */
|
|
2995
|
+
function removeClause(query, index) {
|
|
2996
|
+
return { clauses: query.clauses.filter((_, at) => at !== index) };
|
|
2997
|
+
}
|
|
2998
|
+
/** Every field the query names, in the order they first appear. */
|
|
2999
|
+
function queryFields(query) {
|
|
3000
|
+
const seen = [];
|
|
3001
|
+
for (const clause of query.clauses) if (clause.type === "field" && !seen.includes(clause.field)) seen.push(clause.field);
|
|
3002
|
+
return seen;
|
|
3003
|
+
}
|
|
3004
|
+
/** The values a field is constrained to, if any — for showing a filter as active. */
|
|
3005
|
+
function fieldValues(query, field) {
|
|
3006
|
+
return query.clauses.flatMap((clause) => clause.type === "field" && clause.field === field && !clause.negated ? clause.values : []);
|
|
3042
3007
|
}
|
|
3043
3008
|
/**
|
|
3044
|
-
*
|
|
3009
|
+
* Fold a cell action into the query.
|
|
3045
3010
|
*
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3048
|
-
*
|
|
3049
|
-
* with
|
|
3011
|
+
* The pair of buttons on a log cell is the fastest path from "I noticed this
|
|
3012
|
+
* value" to a filtered view, and the reason it works is that the result is a
|
|
3013
|
+
* query the reader can then see, edit and share — not a hidden filter stack
|
|
3014
|
+
* with its own rules.
|
|
3050
3015
|
*/
|
|
3051
|
-
function
|
|
3052
|
-
|
|
3053
|
-
timeZone: options.timeZone,
|
|
3054
|
-
now: options.now
|
|
3055
|
-
});
|
|
3056
|
-
if (window.isInvalid) return {
|
|
3057
|
-
text: labels.invalid,
|
|
3058
|
-
isInvalid: true
|
|
3059
|
-
};
|
|
3060
|
-
const fromParts = (0, _reopt_ai_opt_charts.parseRelativeParts)(range.from);
|
|
3061
|
-
const toParts = (0, _reopt_ai_opt_charts.parseRelativeParts)(range.to);
|
|
3062
|
-
if ((fromParts === null || fromParts === void 0 ? void 0 : fromParts.roundUnit) && fromParts.count === 0 && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === fromParts.roundUnit && toParts.count === 0) return {
|
|
3063
|
-
text: labels.thisUnit((0, _reopt_ai_opt_charts.formatCountUnit)(1, fromParts.roundUnit, options.locale).replace(/^1\s*/, "")),
|
|
3064
|
-
isInvalid: false
|
|
3065
|
-
};
|
|
3066
|
-
if ((0, _reopt_ai_opt_charts.isRelativeToNow)(range)) {
|
|
3067
|
-
var _toParts$count;
|
|
3068
|
-
const anchoredAtEnd = ((_toParts$count = toParts === null || toParts === void 0 ? void 0 : toParts.count) !== null && _toParts$count !== void 0 ? _toParts$count : 0) === 0;
|
|
3069
|
-
const parts = anchoredAtEnd ? fromParts : toParts;
|
|
3070
|
-
const bothSnapped = (parts === null || parts === void 0 ? void 0 : parts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit) && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit);
|
|
3071
|
-
const counted = parts && parts.count > 0 ? (0, _reopt_ai_opt_charts.formatCountUnit)(parts.roundUnit && bothSnapped ? parts.count + 1 : parts.count, parts.unit, options.locale) : null;
|
|
3072
|
-
const exact = counted !== null && (!(parts === null || parts === void 0 ? void 0 : parts.roundUnit) || bothSnapped);
|
|
3073
|
-
const duration = exact ? counted : (0, _reopt_ai_opt_charts.formatDuration)(window.duration, options.locale);
|
|
3074
|
-
const prefix = exact || (0, _reopt_ai_opt_charts.isExactMinutes)(window.duration) ? "" : "~";
|
|
3075
|
-
return {
|
|
3076
|
-
text: anchoredAtEnd ? labels.last(`${prefix}${duration}`) : labels.next(`${prefix}${duration}`),
|
|
3077
|
-
isInvalid: false
|
|
3078
|
-
};
|
|
3079
|
-
}
|
|
3080
|
-
const dateOnly = (0, _reopt_ai_opt_charts.isDateOnly)(range.from) && (0, _reopt_ai_opt_charts.isDateOnly)(range.to);
|
|
3081
|
-
const from = (0, _reopt_ai_opt_charts.formatInstant)(window.from, require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, options), {}, { dateOnly }));
|
|
3082
|
-
const to = (0, _reopt_ai_opt_charts.formatInstant)(window.to, require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, options), {}, { dateOnly }));
|
|
3083
|
-
return {
|
|
3084
|
-
text: from === to ? from : `${from} – ${to}`,
|
|
3085
|
-
isInvalid: false
|
|
3086
|
-
};
|
|
3016
|
+
function applyCellAction(query, action) {
|
|
3017
|
+
return withFieldValue(query, action.field, action.value, { negated: action.intent === "exclude" });
|
|
3087
3018
|
}
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/lib/query/query-parse.ts
|
|
3021
|
+
const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
3088
3022
|
/**
|
|
3089
|
-
*
|
|
3090
|
-
*
|
|
3091
|
-
* Holding `{ from: "now-7d", to: "now" }` instead of a resolved pair is what
|
|
3092
|
-
* makes a saved board, a shared link and an auto-refresh timer all mean the
|
|
3093
|
-
* same thing tomorrow that they mean today. Around that sit the moves people
|
|
3094
|
-
* actually make on a chart: jump to a named window, step the same width back
|
|
3095
|
-
* and forth to compare, widen when the interesting thing is just off screen,
|
|
3096
|
-
* and let it re-query on a timer while they watch.
|
|
3097
|
-
*
|
|
3098
|
-
* @example
|
|
3099
|
-
* ```tsx
|
|
3100
|
-
* const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
|
|
3023
|
+
* Split on whitespace, but keep quoted runs and parenthesised groups whole.
|
|
3101
3024
|
*
|
|
3102
|
-
*
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
* timeZone="Asia/Seoul"
|
|
3106
|
-
* showWindowButtons
|
|
3107
|
-
* onRefresh={() => refetch()}
|
|
3108
|
-
* />
|
|
3109
|
-
* ```
|
|
3025
|
+
* An unterminated quote or group swallows the rest of the line rather than
|
|
3026
|
+
* erroring: the reader is mid-word, and the alternative is the query flickering
|
|
3027
|
+
* between two different meanings as they type the closing character.
|
|
3110
3028
|
*/
|
|
3111
|
-
function
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
if (!Number.isFinite(count) || count <= 0) return;
|
|
3150
|
-
commit({
|
|
3151
|
-
from: `now-${Math.round(count)}${quickUnit}`,
|
|
3152
|
-
to: "now"
|
|
3153
|
-
});
|
|
3154
|
-
setOpen(false);
|
|
3155
|
-
}, [
|
|
3156
|
-
quickCount,
|
|
3157
|
-
quickUnit,
|
|
3158
|
-
commit
|
|
3159
|
-
]);
|
|
3160
|
-
const step = (0, react.useCallback)((direction) => {
|
|
3161
|
-
const next = (0, _reopt_ai_opt_charts.shiftTimeRange)(value, direction, { timeZone });
|
|
3162
|
-
if (next) commit(next);
|
|
3163
|
-
}, [
|
|
3164
|
-
value,
|
|
3165
|
-
timeZone,
|
|
3166
|
-
commit
|
|
3167
|
-
]);
|
|
3168
|
-
const zoom = (0, react.useCallback)((factor) => {
|
|
3169
|
-
const next = (0, _reopt_ai_opt_charts.zoomTimeRange)(value, factor, { timeZone });
|
|
3170
|
-
if (next) commit(next);
|
|
3171
|
-
}, [
|
|
3172
|
-
value,
|
|
3173
|
-
timeZone,
|
|
3174
|
-
commit
|
|
3175
|
-
]);
|
|
3176
|
-
useAutoRefresh({
|
|
3177
|
-
interval: (_refresh$interval = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval !== void 0 ? _refresh$interval : 0,
|
|
3178
|
-
paused: (_refresh$paused = refresh === null || refresh === void 0 ? void 0 : refresh.paused) !== null && _refresh$paused !== void 0 ? _refresh$paused : true,
|
|
3179
|
-
onRefresh
|
|
3180
|
-
});
|
|
3181
|
-
const refreshLabel = !refresh || refresh.paused || refresh.interval === 0 ? labels.autoRefreshOff : labels.every((0, _reopt_ai_opt_charts.formatDuration)(refresh.interval, locale));
|
|
3182
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3183
|
-
className: require_text_truncate.cn("bg-surface inline-flex items-center rounded-lg border", require_text_truncate.OPT_BORDER, className),
|
|
3184
|
-
role: "group",
|
|
3185
|
-
"aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.trigger,
|
|
3186
|
-
"data-opt-id": "3WJ8H",
|
|
3187
|
-
"data-opt-slug": "time-range-control.TimeRangeControl",
|
|
3188
|
-
children: [
|
|
3189
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverRoot, {
|
|
3190
|
-
open,
|
|
3191
|
-
onOpenChange: setOpen,
|
|
3192
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverTrigger, {
|
|
3193
|
-
className: require_text_truncate.cn("text-text-primary h-10 gap-2", description.isInvalid && "text-danger-fg"),
|
|
3194
|
-
disabled,
|
|
3195
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Clock, {
|
|
3196
|
-
className: "text-text-tertiary h-4 w-4",
|
|
3197
|
-
"aria-hidden": "true"
|
|
3198
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3199
|
-
className: "truncate text-sm",
|
|
3200
|
-
children: description.text
|
|
3201
|
-
})]
|
|
3202
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverContent, {
|
|
3203
|
-
className: "gap-group flex w-80 flex-col",
|
|
3204
|
-
children: [
|
|
3205
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3206
|
-
className: "gap-element flex flex-col",
|
|
3207
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3208
|
-
className: "text-text-primary text-xs font-medium",
|
|
3209
|
-
children: labels.quickSelect
|
|
3210
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3211
|
-
className: "flex items-end gap-2",
|
|
3212
|
-
children: [
|
|
3213
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3214
|
-
"aria-label": labels.quickSelect,
|
|
3215
|
-
type: "number",
|
|
3216
|
-
min: 1,
|
|
3217
|
-
size: "sm",
|
|
3218
|
-
className: "w-20",
|
|
3219
|
-
value: quickCount,
|
|
3220
|
-
onChange: (event) => setQuickCount(event.target.value),
|
|
3221
|
-
onKeyDown: (event) => {
|
|
3222
|
-
if (event.key === "Enter") applyQuick();
|
|
3223
|
-
}
|
|
3224
|
-
}),
|
|
3225
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.SelectRoot, {
|
|
3226
|
-
value: quickUnit,
|
|
3227
|
-
setValue: (next) => setQuickUnit(selectValue(next)),
|
|
3228
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectTrigger, {
|
|
3229
|
-
minWidth: 110,
|
|
3230
|
-
"aria-label": labels.quickSelect
|
|
3231
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectPopover, { children: QUICK_UNITS.map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectItem, {
|
|
3232
|
-
value: unit,
|
|
3233
|
-
children: labels.units[unit]
|
|
3234
|
-
}, unit)) })]
|
|
3235
|
-
}),
|
|
3236
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
3237
|
-
variant: "secondary",
|
|
3238
|
-
size: "sm",
|
|
3239
|
-
onClick: applyQuick,
|
|
3240
|
-
children: labels.apply
|
|
3241
|
-
})
|
|
3242
|
-
]
|
|
3243
|
-
})]
|
|
3244
|
-
}),
|
|
3245
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3246
|
-
className: "gap-element flex flex-col",
|
|
3247
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3248
|
-
label: labels.start,
|
|
3249
|
-
size: "sm",
|
|
3250
|
-
value: editing.from,
|
|
3251
|
-
error: boundState(editing.from),
|
|
3252
|
-
hint: labels.startHint,
|
|
3253
|
-
onChange: (event) => setDraft(require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, editing), {}, { from: event.target.value })),
|
|
3254
|
-
onKeyDown: commitOnEnter
|
|
3255
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3256
|
-
label: labels.end,
|
|
3257
|
-
size: "sm",
|
|
3258
|
-
value: editing.to,
|
|
3259
|
-
error: boundState(editing.to),
|
|
3260
|
-
onChange: (event) => setDraft(require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, editing), {}, { to: event.target.value })),
|
|
3261
|
-
onKeyDown: commitOnEnter
|
|
3262
|
-
})]
|
|
3263
|
-
}),
|
|
3264
|
-
presets && presets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeList, {
|
|
3265
|
-
title: labels.commonlyUsed,
|
|
3266
|
-
items: presets,
|
|
3267
|
-
onPick: (range) => {
|
|
3268
|
-
commit(range);
|
|
3269
|
-
setOpen(false);
|
|
3270
|
-
}
|
|
3271
|
-
}),
|
|
3272
|
-
recentRanges && recentRanges.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeList, {
|
|
3273
|
-
title: labels.recentlyUsed,
|
|
3274
|
-
items: recentRanges.slice(0, 5),
|
|
3275
|
-
onPick: (range) => {
|
|
3276
|
-
commit(range);
|
|
3277
|
-
setOpen(false);
|
|
3278
|
-
}
|
|
3279
|
-
}),
|
|
3280
|
-
onRefreshChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3281
|
-
className: "gap-element flex flex-col",
|
|
3282
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3283
|
-
className: "text-text-primary text-xs font-medium",
|
|
3284
|
-
children: labels.autoRefresh
|
|
3285
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.SelectRoot, {
|
|
3286
|
-
value: String((refresh === null || refresh === void 0 ? void 0 : refresh.paused) ? 0 : (_refresh$interval2 = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval2 !== void 0 ? _refresh$interval2 : 0),
|
|
3287
|
-
setValue: (next) => {
|
|
3288
|
-
const interval = Number(selectValue(next));
|
|
3289
|
-
onRefreshChange({
|
|
3290
|
-
interval,
|
|
3291
|
-
paused: interval === 0
|
|
3292
|
-
});
|
|
3293
|
-
},
|
|
3294
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectTrigger, {
|
|
3295
|
-
minWidth: 140,
|
|
3296
|
-
"aria-label": labels.autoRefresh
|
|
3297
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectPopover, { children: REFRESH_INTERVALS.map((interval) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectItem, {
|
|
3298
|
-
value: String(interval),
|
|
3299
|
-
children: interval === 0 ? labels.autoRefreshOff : labels.every((0, _reopt_ai_opt_charts.formatDuration)(interval, locale))
|
|
3300
|
-
}, interval)) })]
|
|
3301
|
-
})]
|
|
3302
|
-
})
|
|
3303
|
-
]
|
|
3304
|
-
})]
|
|
3305
|
-
}),
|
|
3306
|
-
showWindowButtons && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3307
|
-
className: require_text_truncate.cn("flex items-center border-l", "border border-border"),
|
|
3308
|
-
role: "group",
|
|
3309
|
-
"aria-label": labels.previous,
|
|
3310
|
-
children: [
|
|
3311
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3312
|
-
label: labels.previous,
|
|
3313
|
-
disabled: disabled || description.isInvalid,
|
|
3314
|
-
onClick: () => step(-1),
|
|
3315
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
|
|
3316
|
-
className: "h-4 w-4",
|
|
3317
|
-
"aria-hidden": "true"
|
|
3318
|
-
})
|
|
3319
|
-
}),
|
|
3320
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3321
|
-
label: labels.zoomIn,
|
|
3322
|
-
disabled: disabled || description.isInvalid,
|
|
3323
|
-
onClick: () => zoom(-zoomFactor),
|
|
3324
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, {
|
|
3325
|
-
className: "h-4 w-4",
|
|
3326
|
-
"aria-hidden": "true"
|
|
3327
|
-
})
|
|
3328
|
-
}),
|
|
3329
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3330
|
-
label: labels.zoomOut,
|
|
3331
|
-
disabled: disabled || description.isInvalid,
|
|
3332
|
-
onClick: () => zoom(zoomFactor),
|
|
3333
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, {
|
|
3334
|
-
className: "h-4 w-4",
|
|
3335
|
-
"aria-hidden": "true"
|
|
3336
|
-
})
|
|
3337
|
-
}),
|
|
3338
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3339
|
-
label: labels.nextWindow,
|
|
3340
|
-
disabled: disabled || description.isInvalid,
|
|
3341
|
-
onClick: () => step(1),
|
|
3342
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
3343
|
-
className: "h-4 w-4",
|
|
3344
|
-
"aria-hidden": "true"
|
|
3345
|
-
})
|
|
3346
|
-
})
|
|
3347
|
-
]
|
|
3348
|
-
}),
|
|
3349
|
-
(onRefresh || showUpdateButton) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3350
|
-
className: require_text_truncate.cn("border-l pl-2 pr-2", "border border-border"),
|
|
3351
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
3352
|
-
variant: hasChanges ? "primary" : "secondary",
|
|
3353
|
-
size: "sm",
|
|
3354
|
-
loading: isLoading,
|
|
3355
|
-
disabled: disabled || description.isInvalid,
|
|
3356
|
-
onClick: apply,
|
|
3357
|
-
leftIcon: hasChanges ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.RefreshCw, {
|
|
3358
|
-
className: "h-3.5 w-3.5",
|
|
3359
|
-
"aria-hidden": "true"
|
|
3360
|
-
}),
|
|
3361
|
-
title: refreshLabel,
|
|
3362
|
-
children: hasChanges ? labels.update : labels.refresh
|
|
3363
|
-
})
|
|
3364
|
-
})
|
|
3365
|
-
]
|
|
3366
|
-
});
|
|
3029
|
+
function tokenize(input) {
|
|
3030
|
+
const tokens = [];
|
|
3031
|
+
let index = 0;
|
|
3032
|
+
while (index < input.length) {
|
|
3033
|
+
while (index < input.length && /\s/.test(input[index])) index += 1;
|
|
3034
|
+
if (index >= input.length) break;
|
|
3035
|
+
const start = index;
|
|
3036
|
+
let text = "";
|
|
3037
|
+
let quoted = false;
|
|
3038
|
+
let atTokenStart = true;
|
|
3039
|
+
let depth = 0;
|
|
3040
|
+
while (index < input.length) {
|
|
3041
|
+
const char = input[index];
|
|
3042
|
+
if (char === "\"") {
|
|
3043
|
+
if (atTokenStart) quoted = true;
|
|
3044
|
+
atTokenStart = false;
|
|
3045
|
+
index += 1;
|
|
3046
|
+
while (index < input.length && input[index] !== "\"") {
|
|
3047
|
+
text += input[index];
|
|
3048
|
+
index += 1;
|
|
3049
|
+
}
|
|
3050
|
+
index += 1;
|
|
3051
|
+
continue;
|
|
3052
|
+
}
|
|
3053
|
+
if (char === "(") depth += 1;
|
|
3054
|
+
if (char === ")") depth = Math.max(0, depth - 1);
|
|
3055
|
+
if (/\s/.test(char) && depth === 0) break;
|
|
3056
|
+
atTokenStart = false;
|
|
3057
|
+
text += char;
|
|
3058
|
+
index += 1;
|
|
3059
|
+
}
|
|
3060
|
+
if (text.length > 0 || quoted) tokens.push({
|
|
3061
|
+
text,
|
|
3062
|
+
at: start,
|
|
3063
|
+
quoted
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
return tokens;
|
|
3367
3067
|
}
|
|
3368
|
-
|
|
3369
|
-
function
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
"aria-label": label,
|
|
3374
|
-
title: label,
|
|
3375
|
-
disabled,
|
|
3376
|
-
onClick,
|
|
3377
|
-
children
|
|
3378
|
-
});
|
|
3068
|
+
/** `(a or b)` → `["a", "b"]`; anything else is a single value. */
|
|
3069
|
+
function readValues(raw) {
|
|
3070
|
+
const trimmed = raw.trim();
|
|
3071
|
+
if (!trimmed.startsWith("(")) return [stripQuotes(trimmed)];
|
|
3072
|
+
return trimmed.replace(/^\(/, "").replace(/\)$/, "").split(/\s+or\s+/i).map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
3379
3073
|
}
|
|
3380
|
-
function
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3384
|
-
className: "text-text-primary text-xs font-medium",
|
|
3385
|
-
children: title
|
|
3386
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3387
|
-
className: "grid grid-cols-2 gap-1",
|
|
3388
|
-
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3389
|
-
type: "button",
|
|
3390
|
-
className: require_text_truncate.cn("hover:bg-bg-subtle w-full cursor-pointer rounded px-2 py-1 text-left text-xs", require_text_truncate.OPT_TEXT_SECONDARY),
|
|
3391
|
-
onClick: () => onPick(item.range),
|
|
3392
|
-
children: item.label
|
|
3393
|
-
}) }, `${item.label}-${index}`))
|
|
3394
|
-
})]
|
|
3395
|
-
});
|
|
3074
|
+
function stripQuotes(value) {
|
|
3075
|
+
if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
|
|
3076
|
+
return value.replace(/^"/, "");
|
|
3396
3077
|
}
|
|
3397
|
-
|
|
3398
|
-
* The windows most analytics tools open with. Exported so a caller gets a
|
|
3399
|
-
* sensible list without inventing date math of their own.
|
|
3400
|
-
*/
|
|
3401
|
-
const DEFAULT_TIME_PRESETS = [
|
|
3078
|
+
const OPERATORS = [
|
|
3402
3079
|
{
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
from: "now-15m",
|
|
3406
|
-
to: "now"
|
|
3407
|
-
}
|
|
3080
|
+
symbol: ">=",
|
|
3081
|
+
operator: "gte"
|
|
3408
3082
|
},
|
|
3409
3083
|
{
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
from: "now-1h",
|
|
3413
|
-
to: "now"
|
|
3414
|
-
}
|
|
3084
|
+
symbol: "<=",
|
|
3085
|
+
operator: "lte"
|
|
3415
3086
|
},
|
|
3416
3087
|
{
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
from: "now-24h",
|
|
3420
|
-
to: "now"
|
|
3421
|
-
}
|
|
3088
|
+
symbol: ":",
|
|
3089
|
+
operator: "eq"
|
|
3422
3090
|
},
|
|
3423
3091
|
{
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
from: "now-7d",
|
|
3427
|
-
to: "now"
|
|
3428
|
-
}
|
|
3092
|
+
symbol: ">",
|
|
3093
|
+
operator: "gt"
|
|
3429
3094
|
},
|
|
3430
3095
|
{
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3096
|
+
symbol: "<",
|
|
3097
|
+
operator: "lt"
|
|
3098
|
+
}
|
|
3099
|
+
];
|
|
3100
|
+
/** Where a field operator starts in the token, or `-1`. */
|
|
3101
|
+
function findOperator(text) {
|
|
3102
|
+
let best = null;
|
|
3103
|
+
for (const { symbol, operator } of OPERATORS) {
|
|
3104
|
+
const index = text.indexOf(symbol);
|
|
3105
|
+
if (index <= 0) continue;
|
|
3106
|
+
if (!best || index < best.index || index === best.index && symbol.length > best.symbol.length) best = {
|
|
3107
|
+
index,
|
|
3108
|
+
symbol,
|
|
3109
|
+
operator
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
return best;
|
|
3113
|
+
}
|
|
3114
|
+
/** Parse, keeping whatever could not be understood as a plain term. */
|
|
3115
|
+
function parseQueryWithDiagnostics(input) {
|
|
3116
|
+
const diagnostics = [];
|
|
3117
|
+
const clauses = [];
|
|
3118
|
+
let trailing = null;
|
|
3119
|
+
for (const token of tokenize(input)) {
|
|
3120
|
+
let text = token.text;
|
|
3121
|
+
let negated = false;
|
|
3122
|
+
if (!token.quoted && text.startsWith("-") && text.length > 1) {
|
|
3123
|
+
negated = true;
|
|
3124
|
+
text = text.slice(1);
|
|
3435
3125
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3126
|
+
const operator = token.quoted ? null : findOperator(text);
|
|
3127
|
+
if (!operator) {
|
|
3128
|
+
if (text.length > 0) clauses.push({
|
|
3129
|
+
type: "term",
|
|
3130
|
+
value: text,
|
|
3131
|
+
negated
|
|
3132
|
+
});
|
|
3133
|
+
continue;
|
|
3442
3134
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3135
|
+
const field = text.slice(0, operator.index);
|
|
3136
|
+
const rawValue = text.slice(operator.index + operator.symbol.length);
|
|
3137
|
+
if (!FIELD_NAME.test(field)) {
|
|
3138
|
+
clauses.push({
|
|
3139
|
+
type: "term",
|
|
3140
|
+
value: text,
|
|
3141
|
+
negated
|
|
3142
|
+
});
|
|
3143
|
+
continue;
|
|
3449
3144
|
}
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3145
|
+
if (rawValue === "") {
|
|
3146
|
+
trailing = {
|
|
3147
|
+
field,
|
|
3148
|
+
operator: operator.operator,
|
|
3149
|
+
value: ""
|
|
3150
|
+
};
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
if (rawValue === "*" && operator.operator === "eq") {
|
|
3154
|
+
clauses.push({
|
|
3155
|
+
type: "field",
|
|
3156
|
+
field,
|
|
3157
|
+
operator: "exists",
|
|
3158
|
+
values: [],
|
|
3159
|
+
negated
|
|
3160
|
+
});
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
if (operator.operator === "eq" && rawValue.length > 2 && rawValue.startsWith("*") && rawValue.endsWith("*")) {
|
|
3164
|
+
clauses.push({
|
|
3165
|
+
type: "field",
|
|
3166
|
+
field,
|
|
3167
|
+
operator: "contains",
|
|
3168
|
+
values: [stripQuotes(rawValue.slice(1, -1))],
|
|
3169
|
+
negated
|
|
3170
|
+
});
|
|
3171
|
+
continue;
|
|
3456
3172
|
}
|
|
3173
|
+
const values = readValues(rawValue);
|
|
3174
|
+
if (values.length === 0) {
|
|
3175
|
+
diagnostics.push({
|
|
3176
|
+
at: token.at,
|
|
3177
|
+
text: token.text,
|
|
3178
|
+
message: "Empty value group"
|
|
3179
|
+
});
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
if (operator.operator !== "eq" && values.length > 1) diagnostics.push({
|
|
3183
|
+
at: token.at,
|
|
3184
|
+
text: token.text,
|
|
3185
|
+
message: "A comparison takes a single value"
|
|
3186
|
+
});
|
|
3187
|
+
const clause = {
|
|
3188
|
+
type: "field",
|
|
3189
|
+
field,
|
|
3190
|
+
operator: operator.operator,
|
|
3191
|
+
values: operator.operator === "eq" ? values : values.slice(0, 1),
|
|
3192
|
+
negated
|
|
3193
|
+
};
|
|
3194
|
+
clauses.push(clause);
|
|
3195
|
+
if (!rawValue.startsWith("(")) {
|
|
3196
|
+
var _values$;
|
|
3197
|
+
trailing = {
|
|
3198
|
+
field,
|
|
3199
|
+
operator: operator.operator,
|
|
3200
|
+
value: (_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : ""
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return {
|
|
3205
|
+
query: { clauses },
|
|
3206
|
+
diagnostics,
|
|
3207
|
+
trailing
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
/** Parse, discarding diagnostics. */
|
|
3211
|
+
function parseQuery(input) {
|
|
3212
|
+
return parseQueryWithDiagnostics(input).query;
|
|
3213
|
+
}
|
|
3214
|
+
//#endregion
|
|
3215
|
+
//#region src/lib/query/query-format.ts
|
|
3216
|
+
const OPERATOR_SYMBOL = {
|
|
3217
|
+
eq: ":",
|
|
3218
|
+
contains: ":",
|
|
3219
|
+
gt: ">",
|
|
3220
|
+
gte: ">=",
|
|
3221
|
+
lt: "<",
|
|
3222
|
+
lte: "<=",
|
|
3223
|
+
exists: ":"
|
|
3224
|
+
};
|
|
3225
|
+
/** Quote a value that would otherwise re-parse as something else. */
|
|
3226
|
+
function quoteValue(value) {
|
|
3227
|
+
return /[\s"()]/.test(value) || value === "" ? `"${value.replace(/"/g, "")}"` : value;
|
|
3228
|
+
}
|
|
3229
|
+
/** One clause as text. */
|
|
3230
|
+
function formatClause(clause) {
|
|
3231
|
+
var _clause$values$2;
|
|
3232
|
+
const prefix = clause.negated ? "-" : "";
|
|
3233
|
+
if (clause.type === "term") return `${prefix}${quoteValue(clause.value)}`;
|
|
3234
|
+
if (clause.operator === "exists") return `${prefix}${clause.field}:*`;
|
|
3235
|
+
if (clause.operator === "contains") {
|
|
3236
|
+
var _clause$values$;
|
|
3237
|
+
return `${prefix}${clause.field}:*${(_clause$values$ = clause.values[0]) !== null && _clause$values$ !== void 0 ? _clause$values$ : ""}*`;
|
|
3238
|
+
}
|
|
3239
|
+
const symbol = OPERATOR_SYMBOL[clause.operator];
|
|
3240
|
+
if (clause.values.length > 1) return `${prefix}${clause.field}${symbol}(${clause.values.map(quoteValue).join(" or ")})`;
|
|
3241
|
+
return `${prefix}${clause.field}${symbol}${quoteValue((_clause$values$2 = clause.values[0]) !== null && _clause$values$2 !== void 0 ? _clause$values$2 : "")}`;
|
|
3242
|
+
}
|
|
3243
|
+
/** The whole query as text. */
|
|
3244
|
+
function formatQuery(query) {
|
|
3245
|
+
return query.clauses.map(formatClause).join(" ");
|
|
3246
|
+
}
|
|
3247
|
+
/** A short human label for a clause chip. */
|
|
3248
|
+
function describeClause(clause) {
|
|
3249
|
+
if (clause.type === "term") return clause.negated ? `not "${clause.value}"` : `"${clause.value}"`;
|
|
3250
|
+
if (clause.operator === "exists") return clause.negated ? `${clause.field} missing` : `${clause.field} present`;
|
|
3251
|
+
if (clause.operator === "contains") {
|
|
3252
|
+
var _clause$values$3;
|
|
3253
|
+
return `${clause.negated ? "not " : ""}${clause.field} ⊃ ${(_clause$values$3 = clause.values[0]) !== null && _clause$values$3 !== void 0 ? _clause$values$3 : ""}`;
|
|
3457
3254
|
}
|
|
3458
|
-
|
|
3255
|
+
const values = clause.values.join(", ");
|
|
3256
|
+
const symbol = OPERATOR_SYMBOL[clause.operator];
|
|
3257
|
+
return `${clause.negated ? "not " : ""}${clause.field} ${symbol === ":" ? "=" : symbol} ${values}`;
|
|
3258
|
+
}
|
|
3459
3259
|
//#endregion
|
|
3460
|
-
//#region src/shells/
|
|
3461
|
-
const defaultLabels$
|
|
3462
|
-
|
|
3463
|
-
|
|
3260
|
+
//#region src/shells/query-bar.tsx
|
|
3261
|
+
const defaultLabels$4 = {
|
|
3262
|
+
placeholder: "Search, or filter with field:value",
|
|
3263
|
+
ariaLabel: "Search query",
|
|
3264
|
+
clear: "Clear query",
|
|
3265
|
+
removeClause: (clause) => `Remove ${clause}`,
|
|
3266
|
+
fieldsHeading: "Fields",
|
|
3267
|
+
valuesHeading: "Values",
|
|
3268
|
+
syntaxHint: null
|
|
3464
3269
|
};
|
|
3270
|
+
const MAX_SUGGESTIONS = 8;
|
|
3465
3271
|
/**
|
|
3466
|
-
*
|
|
3272
|
+
* QueryBar — one control for "find this" and "only these".
|
|
3467
3273
|
*
|
|
3468
|
-
*
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
3471
|
-
*
|
|
3472
|
-
*
|
|
3473
|
-
*
|
|
3274
|
+
* A search box and a row of filter pickers answer halves of the same question
|
|
3275
|
+
* and neither can express the other, so people end up using both and holding
|
|
3276
|
+
* the combination in their head. Typing the filter is what closes that: the
|
|
3277
|
+
* whole question is one string, which means it can be read back, corrected in
|
|
3278
|
+
* place, pasted to a colleague, and stored in a URL.
|
|
3279
|
+
*
|
|
3280
|
+
* The text is the source of truth while the reader is typing; the parsed query
|
|
3281
|
+
* is committed on Enter or blur. Anything half-written parses to a plain term
|
|
3282
|
+
* rather than an error, so the bar never argues with someone mid-word.
|
|
3474
3283
|
*
|
|
3475
3284
|
* @example
|
|
3476
3285
|
* ```tsx
|
|
3477
|
-
*
|
|
3478
|
-
*
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3481
|
-
*
|
|
3482
|
-
* series={[{ dataKey: "sessions", name: "Sessions" }]}
|
|
3483
|
-
* range={range}
|
|
3484
|
-
* onRangeChange={setRange}
|
|
3485
|
-
* onRefresh={() => refetch()}
|
|
3486
|
-
* toggleableLegend
|
|
3286
|
+
* <QueryBar
|
|
3287
|
+
* value={query}
|
|
3288
|
+
* onChange={setQuery}
|
|
3289
|
+
* fields={[{ name: "path", values: ["/checkout", "/cart"] }]}
|
|
3290
|
+
* showClauses
|
|
3487
3291
|
* />
|
|
3488
3292
|
* ```
|
|
3489
3293
|
*/
|
|
3490
|
-
function
|
|
3491
|
-
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$
|
|
3492
|
-
const
|
|
3493
|
-
const [
|
|
3494
|
-
const
|
|
3495
|
-
const
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3294
|
+
function QueryBar({ value, onChange, fields = [], showClauses = false, live = false, disabled = false, className, labels: customLabels }) {
|
|
3295
|
+
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$4), customLabels);
|
|
3296
|
+
const inputRef = (0, react.useRef)(null);
|
|
3297
|
+
const [text, setText] = (0, react.useState)(() => formatQuery(value));
|
|
3298
|
+
const [focused, setFocused] = (0, react.useState)(false);
|
|
3299
|
+
const [highlighted, setHighlighted] = (0, react.useState)(0);
|
|
3300
|
+
const committedRef = (0, react.useRef)(formatQuery(value));
|
|
3301
|
+
(0, react.useEffect)(() => {
|
|
3302
|
+
const next = formatQuery(value);
|
|
3303
|
+
if (next === committedRef.current) return;
|
|
3304
|
+
committedRef.current = next;
|
|
3305
|
+
if (!focused) setText(next);
|
|
3306
|
+
}, [value, focused]);
|
|
3307
|
+
const parsed = (0, react.useMemo)(() => parseQueryWithDiagnostics(text), [text]);
|
|
3308
|
+
const commit = (0, react.useCallback)((nextText) => {
|
|
3309
|
+
const nextQuery = parseQueryWithDiagnostics(nextText).query;
|
|
3310
|
+
committedRef.current = formatQuery(nextQuery);
|
|
3311
|
+
onChange(nextQuery);
|
|
3312
|
+
}, [onChange]);
|
|
3313
|
+
const suggestions = useSuggestions(text, parsed.trailing, fields);
|
|
3314
|
+
const applySuggestion = (0, react.useCallback)((suggestion) => {
|
|
3315
|
+
var _inputRef$current;
|
|
3316
|
+
const nextText = suggestion.replace(text);
|
|
3317
|
+
setText(nextText);
|
|
3318
|
+
if (suggestion.kind === "value") commit(nextText);
|
|
3319
|
+
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
3320
|
+
setHighlighted(0);
|
|
3321
|
+
}, [text, commit]);
|
|
3322
|
+
const handleKeyDown = (event) => {
|
|
3323
|
+
if (suggestions.length > 0 && focused) {
|
|
3324
|
+
if (event.key === "ArrowDown") {
|
|
3325
|
+
event.preventDefault();
|
|
3326
|
+
setHighlighted((current) => (current + 1) % suggestions.length);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
if (event.key === "ArrowUp") {
|
|
3330
|
+
event.preventDefault();
|
|
3331
|
+
setHighlighted((current) => (current - 1 + suggestions.length) % suggestions.length);
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
if (event.key === "Tab" || event.key === "Enter" && highlighted > 0) {
|
|
3335
|
+
event.preventDefault();
|
|
3336
|
+
applySuggestion(suggestions[highlighted]);
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
if (event.key === "Enter") {
|
|
3341
|
+
event.preventDefault();
|
|
3342
|
+
commit(text);
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
if (event.key === "Escape") {
|
|
3346
|
+
var _inputRef$current2;
|
|
3347
|
+
setText(committedRef.current);
|
|
3348
|
+
(_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
const showSuggestions = focused && suggestions.length > 0;
|
|
3352
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3353
|
+
className: require_text_truncate.cn("relative flex min-w-0 flex-col gap-1.5", className),
|
|
3354
|
+
"data-opt-id": "JSVZ1",
|
|
3355
|
+
"data-opt-slug": "query-bar.QueryBar",
|
|
3356
|
+
children: [
|
|
3357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3358
|
+
className: require_text_truncate.cn("bg-surface flex h-9 items-center gap-2 rounded-md border px-2.5", require_text_truncate.OPT_BORDER, "focus-within:border-accent transition-colors", disabled && "opacity-60"),
|
|
3532
3359
|
children: [
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
onClick: goBack,
|
|
3537
|
-
leftIcon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, {
|
|
3538
|
-
className: "h-3.5 w-3.5",
|
|
3539
|
-
"aria-hidden": "true"
|
|
3540
|
-
}),
|
|
3541
|
-
children: labels.back
|
|
3360
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Search, {
|
|
3361
|
+
className: "text-text-tertiary size-4 shrink-0",
|
|
3362
|
+
"aria-hidden": true
|
|
3542
3363
|
}),
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3365
|
+
ref: inputRef,
|
|
3366
|
+
type: "text",
|
|
3367
|
+
role: "combobox",
|
|
3368
|
+
"aria-expanded": showSuggestions,
|
|
3369
|
+
"aria-controls": "query-bar-suggestions",
|
|
3370
|
+
"aria-autocomplete": "list",
|
|
3371
|
+
"aria-label": labels.ariaLabel,
|
|
3372
|
+
placeholder: labels.placeholder,
|
|
3373
|
+
disabled,
|
|
3374
|
+
className: require_text_truncate.cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent font-mono text-sm outline-none focus-visible:outline-none"),
|
|
3375
|
+
value: text,
|
|
3376
|
+
onChange: (event) => {
|
|
3377
|
+
setText(event.target.value);
|
|
3378
|
+
setHighlighted(0);
|
|
3379
|
+
if (live) commit(event.target.value);
|
|
3380
|
+
},
|
|
3381
|
+
onFocus: () => setFocused(true),
|
|
3382
|
+
onBlur: () => {
|
|
3383
|
+
window.setTimeout(() => setFocused(false), 120);
|
|
3384
|
+
if (!live) commit(text);
|
|
3385
|
+
},
|
|
3386
|
+
onKeyDown: handleKeyDown
|
|
3387
|
+
}),
|
|
3388
|
+
text.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3389
|
+
type: "button",
|
|
3390
|
+
"aria-label": labels.clear,
|
|
3391
|
+
title: labels.clear,
|
|
3392
|
+
className: require_text_truncate.cn("text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5", "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset"),
|
|
3393
|
+
onClick: () => {
|
|
3394
|
+
setText("");
|
|
3395
|
+
commit("");
|
|
3396
|
+
},
|
|
3397
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {
|
|
3398
|
+
className: "size-3.5",
|
|
3399
|
+
"aria-hidden": true
|
|
3400
|
+
})
|
|
3556
3401
|
})
|
|
3557
3402
|
]
|
|
3558
|
-
})
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
},
|
|
3580
|
-
|
|
3403
|
+
}),
|
|
3404
|
+
showSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3405
|
+
id: "query-bar-suggestions",
|
|
3406
|
+
role: "listbox",
|
|
3407
|
+
className: require_text_truncate.cn("bg-surface-overlay absolute top-10 z-40 max-h-64 w-full overflow-y-auto rounded-md border p-1 shadow-lg", "border border-border"),
|
|
3408
|
+
children: suggestions.map((suggestion, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3409
|
+
type: "button",
|
|
3410
|
+
role: "option",
|
|
3411
|
+
"aria-selected": index === highlighted,
|
|
3412
|
+
className: require_text_truncate.cn("flex w-full cursor-pointer items-center justify-between gap-3 rounded px-2 py-1 text-left text-sm", index === highlighted ? "bg-bg-subtle" : "hover:bg-bg-subtle"),
|
|
3413
|
+
onMouseEnter: () => setHighlighted(index),
|
|
3414
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
3415
|
+
onClick: () => applySuggestion(suggestion),
|
|
3416
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3417
|
+
className: "text-text-primary truncate font-mono",
|
|
3418
|
+
children: suggestion.label
|
|
3419
|
+
}), suggestion.hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3420
|
+
className: "text-text-tertiary shrink-0 text-xs",
|
|
3421
|
+
children: suggestion.hint
|
|
3422
|
+
})]
|
|
3423
|
+
}) }, `${suggestion.kind}-${suggestion.label}`))
|
|
3424
|
+
}),
|
|
3425
|
+
showClauses && value.clauses.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3426
|
+
className: "flex flex-wrap items-center gap-1.5",
|
|
3427
|
+
children: value.clauses.map((clause, index) => {
|
|
3428
|
+
const label = describeClause(clause);
|
|
3429
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
3430
|
+
className: require_text_truncate.cn("bg-bg-subtle text-text-secondary flex items-center gap-1 rounded px-1.5 py-0.5 text-xs", clause.negated && "text-danger-fg"),
|
|
3431
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3432
|
+
className: "truncate",
|
|
3433
|
+
children: label
|
|
3434
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3435
|
+
type: "button",
|
|
3436
|
+
"aria-label": labels.removeClause(label),
|
|
3437
|
+
className: "hover:text-text-primary cursor-pointer",
|
|
3438
|
+
onClick: () => {
|
|
3439
|
+
const next = removeClause(value, index);
|
|
3440
|
+
setText(formatQuery(next));
|
|
3441
|
+
committedRef.current = formatQuery(next);
|
|
3442
|
+
onChange(next);
|
|
3443
|
+
},
|
|
3444
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {
|
|
3445
|
+
className: "size-3",
|
|
3446
|
+
"aria-hidden": true
|
|
3447
|
+
})
|
|
3448
|
+
})]
|
|
3449
|
+
}, `${label}-${index}`);
|
|
3450
|
+
})
|
|
3451
|
+
}),
|
|
3452
|
+
parsed.diagnostics.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QueryDiagnostics, { diagnostics: parsed.diagnostics }),
|
|
3453
|
+
labels.syntaxHint && !showSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3454
|
+
className: "text-text-tertiary text-xs",
|
|
3455
|
+
children: labels.syntaxHint
|
|
3456
|
+
})
|
|
3457
|
+
]
|
|
3581
3458
|
});
|
|
3582
3459
|
}
|
|
3583
|
-
|
|
3460
|
+
QueryBar.displayName = "QueryBar";
|
|
3461
|
+
function QueryDiagnostics({ diagnostics }) {
|
|
3462
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3463
|
+
className: "text-warning-fg flex flex-col gap-0.5 text-xs",
|
|
3464
|
+
children: diagnostics.map((diagnostic, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
3465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
3466
|
+
className: "font-mono",
|
|
3467
|
+
children: diagnostic.text
|
|
3468
|
+
}),
|
|
3469
|
+
" —",
|
|
3470
|
+
" ",
|
|
3471
|
+
diagnostic.message
|
|
3472
|
+
] }, `${diagnostic.at}-${index}`))
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* What to offer, based on the token under the cursor.
|
|
3477
|
+
*
|
|
3478
|
+
* Completing fields is the part that makes the syntax discoverable at all —
|
|
3479
|
+
* nobody guesses that `utm_source` is a field they can filter on. Once the
|
|
3480
|
+
* field is chosen, its known values are the second half of the same guess.
|
|
3481
|
+
*/
|
|
3482
|
+
function useSuggestions(text, trailing, fields) {
|
|
3483
|
+
return (0, react.useMemo)(() => {
|
|
3484
|
+
const lastSpace = text.lastIndexOf(" ");
|
|
3485
|
+
const head = lastSpace === -1 ? "" : text.slice(0, lastSpace + 1);
|
|
3486
|
+
const token = text.slice(lastSpace + 1);
|
|
3487
|
+
if (trailing) {
|
|
3488
|
+
var _field$values;
|
|
3489
|
+
const field = fields.find((candidate) => candidate.name === trailing.field);
|
|
3490
|
+
const values = (_field$values = field === null || field === void 0 ? void 0 : field.values) !== null && _field$values !== void 0 ? _field$values : [];
|
|
3491
|
+
const typed = trailing.value.toLowerCase();
|
|
3492
|
+
return values.filter((value) => value.toLowerCase().includes(typed)).slice(0, MAX_SUGGESTIONS).map((value) => ({
|
|
3493
|
+
kind: "value",
|
|
3494
|
+
label: `${trailing.field}:${value}`,
|
|
3495
|
+
replace: () => `${head}${trailing.field}:${/[\s"()]/.test(value) ? `"${value}"` : value} `
|
|
3496
|
+
}));
|
|
3497
|
+
}
|
|
3498
|
+
const bare = token.replace(/^-/, "").toLowerCase();
|
|
3499
|
+
if (bare.length === 0) return [];
|
|
3500
|
+
return fields.filter((field) => field.name.toLowerCase().includes(bare)).slice(0, MAX_SUGGESTIONS).map((field) => ({
|
|
3501
|
+
kind: "field",
|
|
3502
|
+
label: `${field.name}:`,
|
|
3503
|
+
hint: field.hint,
|
|
3504
|
+
replace: () => `${head}${token.startsWith("-") ? "-" : ""}${field.name}:`
|
|
3505
|
+
}));
|
|
3506
|
+
}, [
|
|
3507
|
+
text,
|
|
3508
|
+
trailing,
|
|
3509
|
+
fields
|
|
3510
|
+
]);
|
|
3511
|
+
}
|
|
3584
3512
|
//#endregion
|
|
3585
|
-
//#region src/
|
|
3586
|
-
const HIDE_BELOW = {
|
|
3587
|
-
always: "",
|
|
3588
|
-
sm: "hidden @[40rem]/log:flex",
|
|
3589
|
-
md: "hidden @[46rem]/log:flex",
|
|
3590
|
-
lg: "hidden @[58rem]/log:flex",
|
|
3591
|
-
xl: "hidden @[70rem]/log:flex"
|
|
3592
|
-
};
|
|
3593
|
-
const ROW_HEIGHT = {
|
|
3594
|
-
compact: 26,
|
|
3595
|
-
normal: 32,
|
|
3596
|
-
comfortable: 40
|
|
3597
|
-
};
|
|
3598
|
-
const DENSITY_TEXT = {
|
|
3599
|
-
compact: "text-[11px]",
|
|
3600
|
-
normal: "text-xs",
|
|
3601
|
-
comfortable: "text-sm"
|
|
3602
|
-
};
|
|
3603
|
-
const defaultLabels$2 = {
|
|
3604
|
-
listLabel: "Log",
|
|
3605
|
-
loadingMore: "Loading earlier entries…",
|
|
3606
|
-
endOfResults: "That is every entry in this range.",
|
|
3607
|
-
filterFor: (value) => `Filter for ${value}`,
|
|
3608
|
-
filterOut: (value) => `Filter out ${value}`,
|
|
3609
|
-
sortAscending: "Sort ascending",
|
|
3610
|
-
sortDescending: "Sort descending",
|
|
3611
|
-
moveLeft: (column) => `Move ${column} left`,
|
|
3612
|
-
moveRight: (column) => `Move ${column} right`
|
|
3613
|
-
};
|
|
3614
|
-
/** Rows rendered beyond the viewport on each side, so a fast scroll stays filled. */
|
|
3615
|
-
const OVERSCAN = 10;
|
|
3513
|
+
//#region src/lib/use-auto-refresh.ts
|
|
3616
3514
|
/**
|
|
3617
|
-
* A
|
|
3618
|
-
*
|
|
3619
|
-
*
|
|
3620
|
-
* a
|
|
3515
|
+
* A refresh timer that waits for its own work.
|
|
3516
|
+
*
|
|
3517
|
+
* A plain `setInterval` fires on a schedule regardless of whether the previous
|
|
3518
|
+
* query came back, so a five-second interval over a seven-second query stacks
|
|
3519
|
+
* requests until something gives. Chaining the next wait onto the settled
|
|
3520
|
+
* promise keeps exactly one refresh in flight, and a rejection stops the loop
|
|
3521
|
+
* instead of hammering an endpoint that is already failing.
|
|
3621
3522
|
*/
|
|
3622
|
-
function
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3523
|
+
function useAutoRefresh({ interval, paused = false, onRefresh }) {
|
|
3524
|
+
const callbackRef = (0, react.useRef)(onRefresh);
|
|
3525
|
+
callbackRef.current = onRefresh;
|
|
3526
|
+
(0, react.useEffect)(() => {
|
|
3527
|
+
if (paused || interval <= 0 || !callbackRef.current) return;
|
|
3528
|
+
let cancelled = false;
|
|
3529
|
+
let timer;
|
|
3530
|
+
const schedule = () => {
|
|
3531
|
+
timer = setTimeout(async () => {
|
|
3532
|
+
if (cancelled) return;
|
|
3533
|
+
try {
|
|
3534
|
+
var _callbackRef$current;
|
|
3535
|
+
await ((_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef));
|
|
3536
|
+
} catch (_unused) {
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
if (!cancelled) schedule();
|
|
3540
|
+
}, interval);
|
|
3541
|
+
};
|
|
3542
|
+
schedule();
|
|
3543
|
+
return () => {
|
|
3544
|
+
cancelled = true;
|
|
3545
|
+
if (timer) clearTimeout(timer);
|
|
3546
|
+
};
|
|
3547
|
+
}, [interval, paused]);
|
|
3548
|
+
}
|
|
3549
|
+
//#endregion
|
|
3550
|
+
//#region src/shells/time-range-control.tsx
|
|
3551
|
+
const defaultLabels$3 = {
|
|
3552
|
+
trigger: "Select time range",
|
|
3553
|
+
invalid: "Invalid time range",
|
|
3554
|
+
last: (duration) => `Last ${duration}`,
|
|
3555
|
+
next: (duration) => `Next ${duration}`,
|
|
3556
|
+
quickSelect: "Quick select",
|
|
3557
|
+
apply: "Apply",
|
|
3558
|
+
update: "Update",
|
|
3559
|
+
refresh: "Refresh",
|
|
3560
|
+
commonlyUsed: "Commonly used",
|
|
3561
|
+
recentlyUsed: "Recently used",
|
|
3562
|
+
start: "Start",
|
|
3563
|
+
end: "End",
|
|
3564
|
+
startHint: "Absolute time or date math, e.g. now-7d",
|
|
3565
|
+
autoRefresh: "Auto refresh",
|
|
3566
|
+
autoRefreshOff: "Off",
|
|
3567
|
+
every: (duration) => `Every ${duration}`,
|
|
3568
|
+
previous: "Previous window",
|
|
3569
|
+
nextWindow: "Next window",
|
|
3570
|
+
zoomIn: "Zoom in",
|
|
3571
|
+
zoomOut: "Zoom out",
|
|
3572
|
+
thisUnit: (unit) => `This ${unit}`,
|
|
3573
|
+
units: {
|
|
3574
|
+
s: "seconds",
|
|
3575
|
+
m: "minutes",
|
|
3576
|
+
h: "hours",
|
|
3577
|
+
d: "days",
|
|
3578
|
+
w: "weeks",
|
|
3579
|
+
M: "months",
|
|
3580
|
+
y: "years"
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
const QUICK_UNITS = [
|
|
3584
|
+
"m",
|
|
3585
|
+
"h",
|
|
3586
|
+
"d",
|
|
3587
|
+
"w",
|
|
3588
|
+
"M",
|
|
3589
|
+
"y"
|
|
3590
|
+
];
|
|
3591
|
+
const REFRESH_INTERVALS = [
|
|
3592
|
+
0,
|
|
3593
|
+
5e3,
|
|
3594
|
+
1e4,
|
|
3595
|
+
3e4,
|
|
3596
|
+
6e4,
|
|
3597
|
+
3e5
|
|
3598
|
+
];
|
|
3599
|
+
function selectValue(value) {
|
|
3600
|
+
var _value$;
|
|
3601
|
+
return Array.isArray(value) ? (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : "" : value;
|
|
3626
3602
|
}
|
|
3627
|
-
const EM_DASH = "—";
|
|
3628
3603
|
/**
|
|
3629
|
-
*
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3604
|
+
* What the trigger says.
|
|
3605
|
+
*
|
|
3606
|
+
* A window anchored to `now` deserves its intent ("Last 7 days"), not two
|
|
3607
|
+
* timestamps the reader has to subtract. Everything else gets both ends
|
|
3608
|
+
* spelled out. A duration that does not divide into whole minutes is prefixed
|
|
3609
|
+
* with `~` rather than rounded silently.
|
|
3632
3610
|
*/
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3611
|
+
function describeRange(range, labels, options) {
|
|
3612
|
+
const window = (0, _reopt_ai_opt_charts.measureTimeWindow)(range, {
|
|
3613
|
+
timeZone: options.timeZone,
|
|
3614
|
+
now: options.now
|
|
3615
|
+
});
|
|
3616
|
+
if (window.isInvalid) return {
|
|
3617
|
+
text: labels.invalid,
|
|
3618
|
+
isInvalid: true
|
|
3619
|
+
};
|
|
3620
|
+
const fromParts = (0, _reopt_ai_opt_charts.parseRelativeParts)(range.from);
|
|
3621
|
+
const toParts = (0, _reopt_ai_opt_charts.parseRelativeParts)(range.to);
|
|
3622
|
+
if ((fromParts === null || fromParts === void 0 ? void 0 : fromParts.roundUnit) && fromParts.count === 0 && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === fromParts.roundUnit && toParts.count === 0) return {
|
|
3623
|
+
text: labels.thisUnit((0, _reopt_ai_opt_charts.formatCountUnit)(1, fromParts.roundUnit, options.locale).replace(/^1\s*/, "")),
|
|
3624
|
+
isInvalid: false
|
|
3625
|
+
};
|
|
3626
|
+
if ((0, _reopt_ai_opt_charts.isRelativeToNow)(range)) {
|
|
3627
|
+
var _toParts$count;
|
|
3628
|
+
const anchoredAtEnd = ((_toParts$count = toParts === null || toParts === void 0 ? void 0 : toParts.count) !== null && _toParts$count !== void 0 ? _toParts$count : 0) === 0;
|
|
3629
|
+
const parts = anchoredAtEnd ? fromParts : toParts;
|
|
3630
|
+
const bothSnapped = (parts === null || parts === void 0 ? void 0 : parts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit) && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit);
|
|
3631
|
+
const counted = parts && parts.count > 0 ? (0, _reopt_ai_opt_charts.formatCountUnit)(parts.roundUnit && bothSnapped ? parts.count + 1 : parts.count, parts.unit, options.locale) : null;
|
|
3632
|
+
const exact = counted !== null && (!(parts === null || parts === void 0 ? void 0 : parts.roundUnit) || bothSnapped);
|
|
3633
|
+
const duration = exact ? counted : (0, _reopt_ai_opt_charts.formatDuration)(window.duration, options.locale);
|
|
3634
|
+
const prefix = exact || (0, _reopt_ai_opt_charts.isExactMinutes)(window.duration) ? "" : "~";
|
|
3635
|
+
return {
|
|
3636
|
+
text: anchoredAtEnd ? labels.last(`${prefix}${duration}`) : labels.next(`${prefix}${duration}`),
|
|
3637
|
+
isInvalid: false
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
const dateOnly = (0, _reopt_ai_opt_charts.isDateOnly)(range.from) && (0, _reopt_ai_opt_charts.isDateOnly)(range.to);
|
|
3641
|
+
const from = (0, _reopt_ai_opt_charts.formatInstant)(window.from, require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, options), {}, { dateOnly }));
|
|
3642
|
+
const to = (0, _reopt_ai_opt_charts.formatInstant)(window.to, require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, options), {}, { dateOnly }));
|
|
3643
|
+
return {
|
|
3644
|
+
text: from === to ? from : `${from} – ${to}`,
|
|
3645
|
+
isInvalid: false
|
|
3641
3646
|
};
|
|
3642
3647
|
}
|
|
3643
3648
|
/**
|
|
3644
|
-
*
|
|
3645
|
-
*
|
|
3646
|
-
* Written by hand rather than delegated to a data grid for the two things a log
|
|
3647
|
-
* needs that a grid does not: rows are inserted at the *top* while the reader
|
|
3648
|
-
* is partway down, which needs a scroll correction applied before paint, and
|
|
3649
|
-
* "am I at the newest row?" has to be answerable from the scroll container to
|
|
3650
|
-
* decide whether to follow the tail or hold position.
|
|
3649
|
+
* TimeRangeControl — the window as an expression, not two frozen instants.
|
|
3651
3650
|
*
|
|
3652
|
-
*
|
|
3653
|
-
*
|
|
3654
|
-
*
|
|
3655
|
-
*
|
|
3651
|
+
* Holding `{ from: "now-7d", to: "now" }` instead of a resolved pair is what
|
|
3652
|
+
* makes a saved board, a shared link and an auto-refresh timer all mean the
|
|
3653
|
+
* same thing tomorrow that they mean today. Around that sit the moves people
|
|
3654
|
+
* actually make on a chart: jump to a named window, step the same width back
|
|
3655
|
+
* and forth to compare, widen when the interesting thing is just off screen,
|
|
3656
|
+
* and let it re-query on a timer while they watch.
|
|
3656
3657
|
*
|
|
3657
3658
|
* @example
|
|
3658
3659
|
* ```tsx
|
|
3659
|
-
* <
|
|
3660
|
-
*
|
|
3661
|
-
*
|
|
3662
|
-
*
|
|
3663
|
-
*
|
|
3664
|
-
*
|
|
3665
|
-
*
|
|
3666
|
-
*
|
|
3667
|
-
* onCellAction={(action) => setQuery(applyCellAction(query, action))}
|
|
3660
|
+
* const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
|
|
3661
|
+
*
|
|
3662
|
+
* <TimeRangeControl
|
|
3663
|
+
* value={range}
|
|
3664
|
+
* onChange={setRange}
|
|
3665
|
+
* timeZone="Asia/Seoul"
|
|
3666
|
+
* showWindowButtons
|
|
3667
|
+
* onRefresh={() => refetch()}
|
|
3668
3668
|
* />
|
|
3669
3669
|
* ```
|
|
3670
3670
|
*/
|
|
3671
|
-
function
|
|
3672
|
-
|
|
3673
|
-
const
|
|
3674
|
-
const
|
|
3675
|
-
const [
|
|
3676
|
-
const [
|
|
3677
|
-
const
|
|
3678
|
-
const
|
|
3679
|
-
const
|
|
3680
|
-
(0, react.
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
if (!container) return;
|
|
3684
|
-
container.scrollTop = 0;
|
|
3685
|
-
setScrollTop(0);
|
|
3686
|
-
if (!followingRef.current) {
|
|
3687
|
-
followingRef.current = true;
|
|
3688
|
-
onFollowChange === null || onFollowChange === void 0 || onFollowChange(true);
|
|
3689
|
-
}
|
|
3690
|
-
},
|
|
3691
|
-
revealRow: (id) => {
|
|
3692
|
-
const index = rows.findIndex((row) => getRowId(row) === id);
|
|
3693
|
-
const container = scrollRef.current;
|
|
3694
|
-
if (index === -1 || !container) return;
|
|
3695
|
-
const top = index * rowHeight;
|
|
3696
|
-
const bottom = top + rowHeight;
|
|
3697
|
-
if (top < container.scrollTop) container.scrollTop = top;
|
|
3698
|
-
else if (bottom > container.scrollTop + container.clientHeight) container.scrollTop = bottom - container.clientHeight;
|
|
3699
|
-
}
|
|
3671
|
+
function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, locale, showUpdateButton = true, showWindowButtons = true, zoomFactor = .5, refresh, onRefreshChange, onRefresh, isLoading = false, disabled = false, className, labels: customLabels, "aria-label": ariaLabel }) {
|
|
3672
|
+
var _refresh$interval, _refresh$paused, _refresh$interval2;
|
|
3673
|
+
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$3), customLabels), {}, { units: require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$3.units), customLabels === null || customLabels === void 0 ? void 0 : customLabels.units) });
|
|
3674
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
3675
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
3676
|
+
const [quickCount, setQuickCount] = (0, react.useState)("15");
|
|
3677
|
+
const [quickUnit, setQuickUnit] = (0, react.useState)("m");
|
|
3678
|
+
const editing = draft !== null && draft !== void 0 ? draft : value;
|
|
3679
|
+
const hasChanges = draft !== null && (draft.from !== value.from || draft.to !== value.to);
|
|
3680
|
+
const description = (0, react.useMemo)(() => describeRange(value, labels, {
|
|
3681
|
+
locale,
|
|
3682
|
+
timeZone
|
|
3700
3683
|
}), [
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
]);
|
|
3706
|
-
(0, react.useEffect)(() => {
|
|
3707
|
-
const container = scrollRef.current;
|
|
3708
|
-
if (!container) return;
|
|
3709
|
-
const observer = new ResizeObserver(() => setViewportHeight(container.clientHeight));
|
|
3710
|
-
observer.observe(container);
|
|
3711
|
-
setViewportHeight(container.clientHeight);
|
|
3712
|
-
return () => observer.disconnect();
|
|
3713
|
-
}, []);
|
|
3714
|
-
/**
|
|
3715
|
-
* Report whether the list is pinned to the newest row.
|
|
3716
|
-
*
|
|
3717
|
-
* Called from more than the scroll handler on purpose: the list can arrive at
|
|
3718
|
-
* the top without a scroll event — a reset, a jump to newest, a shorter
|
|
3719
|
-
* result set — and a stale "not following" would silently divert every live
|
|
3720
|
-
* row into a held-back pile.
|
|
3721
|
-
*/
|
|
3722
|
-
const syncFollowing = (0, react.useCallback)((container) => {
|
|
3723
|
-
const following = container.scrollTop <= rowHeight;
|
|
3724
|
-
if (following === followingRef.current) return;
|
|
3725
|
-
followingRef.current = following;
|
|
3726
|
-
onFollowChange === null || onFollowChange === void 0 || onFollowChange(following);
|
|
3727
|
-
}, [onFollowChange, rowHeight]);
|
|
3728
|
-
/**
|
|
3729
|
-
* Hold the reader's place when rows arrive above them.
|
|
3730
|
-
*
|
|
3731
|
-
* Runs before paint: by the time the browser draws, the scroll offset has
|
|
3732
|
-
* already absorbed the inserted height, so the content under the cursor never
|
|
3733
|
-
* moves. Skipped while following, where the whole point is to stay at the top.
|
|
3734
|
-
*/
|
|
3735
|
-
(0, react.useLayoutEffect)(() => {
|
|
3736
|
-
const container = scrollRef.current;
|
|
3737
|
-
const previousTopId = previousTopIdRef.current;
|
|
3738
|
-
previousTopIdRef.current = rows.length ? getRowId(rows[0]) : null;
|
|
3739
|
-
const previousLength = previousLengthRef.current;
|
|
3740
|
-
previousLengthRef.current = rows.length;
|
|
3741
|
-
if (!container) return;
|
|
3742
|
-
syncFollowing(container);
|
|
3743
|
-
if (!prepending || followingRef.current || !previousTopId) return;
|
|
3744
|
-
if (rows.length <= previousLength) return;
|
|
3745
|
-
const prepended = rows.findIndex((row) => getRowId(row) === previousTopId);
|
|
3746
|
-
if (prepended <= 0) return;
|
|
3747
|
-
container.scrollTop += prepended * rowHeight;
|
|
3748
|
-
setScrollTop(container.scrollTop);
|
|
3749
|
-
}, [
|
|
3750
|
-
rows,
|
|
3751
|
-
getRowId,
|
|
3752
|
-
prepending,
|
|
3753
|
-
rowHeight,
|
|
3754
|
-
syncFollowing
|
|
3755
|
-
]);
|
|
3756
|
-
/**
|
|
3757
|
-
* Page forward when the rows do not reach the bottom of the viewport.
|
|
3758
|
-
*
|
|
3759
|
-
* Without this the list only advances on a scroll event, so a tall screen —
|
|
3760
|
-
* or a filter matching few rows per page — stops at the first page with no
|
|
3761
|
-
* way to ask for more.
|
|
3762
|
-
*/
|
|
3763
|
-
(0, react.useEffect)(() => {
|
|
3764
|
-
const container = scrollRef.current;
|
|
3765
|
-
if (!container || !hasMore || loadingMore || !onLoadMore) return;
|
|
3766
|
-
if (container.scrollHeight <= container.clientHeight) onLoadMore();
|
|
3767
|
-
}, [
|
|
3768
|
-
hasMore,
|
|
3769
|
-
loadingMore,
|
|
3770
|
-
onLoadMore,
|
|
3771
|
-
rows.length,
|
|
3772
|
-
viewportHeight
|
|
3684
|
+
value,
|
|
3685
|
+
locale,
|
|
3686
|
+
timeZone,
|
|
3687
|
+
customLabels
|
|
3773
3688
|
]);
|
|
3774
|
-
const
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3689
|
+
const boundState = (0, react.useCallback)((expression) => (0, _reopt_ai_opt_charts.parseTimeExpression)(expression, { timeZone }) === null ? labels.invalid : void 0, [timeZone, labels]);
|
|
3690
|
+
const commit = (0, react.useCallback)((range) => {
|
|
3691
|
+
setDraft(null);
|
|
3692
|
+
onChange(range);
|
|
3693
|
+
}, [onChange]);
|
|
3694
|
+
const apply = (0, react.useCallback)(() => {
|
|
3695
|
+
if (draft) commit(draft);
|
|
3696
|
+
else onRefresh === null || onRefresh === void 0 || onRefresh();
|
|
3780
3697
|
}, [
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
syncFollowing
|
|
3785
|
-
]);
|
|
3786
|
-
const totalHeight = rows.length * rowHeight;
|
|
3787
|
-
const firstVisible = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
|
|
3788
|
-
const visibleCount = Math.ceil((viewportHeight || 480) / rowHeight) + 20;
|
|
3789
|
-
const lastVisible = Math.min(rows.length, firstVisible + visibleCount);
|
|
3790
|
-
const visibleRows = (0, react.useMemo)(() => rows.slice(firstVisible, lastVisible), [
|
|
3791
|
-
rows,
|
|
3792
|
-
firstVisible,
|
|
3793
|
-
lastVisible
|
|
3698
|
+
draft,
|
|
3699
|
+
commit,
|
|
3700
|
+
onRefresh
|
|
3794
3701
|
]);
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
})
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
labels
|
|
3834
|
-
}, id);
|
|
3835
|
-
})
|
|
3836
|
-
}),
|
|
3837
|
-
loadingMore && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3838
|
-
className: "gap-element text-text-tertiary flex items-center justify-center py-3 text-xs",
|
|
3839
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Spinner, {
|
|
3840
|
-
size: "sm",
|
|
3841
|
-
"aria-hidden": true
|
|
3842
|
-
}), labels.loadingMore]
|
|
3843
|
-
}),
|
|
3844
|
-
!hasMore && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3845
|
-
className: require_text_truncate.cn("text-text-tertiary", "py-3 text-center text-xs"),
|
|
3846
|
-
children: labels.endOfResults
|
|
3847
|
-
})
|
|
3848
|
-
]
|
|
3849
|
-
})]
|
|
3702
|
+
const commitOnEnter = (0, react.useCallback)((event) => {
|
|
3703
|
+
if (event.key !== "Enter") return;
|
|
3704
|
+
apply();
|
|
3705
|
+
setOpen(false);
|
|
3706
|
+
}, [apply]);
|
|
3707
|
+
const applyQuick = (0, react.useCallback)(() => {
|
|
3708
|
+
const count = Number(quickCount);
|
|
3709
|
+
if (!Number.isFinite(count) || count <= 0) return;
|
|
3710
|
+
commit({
|
|
3711
|
+
from: `now-${Math.round(count)}${quickUnit}`,
|
|
3712
|
+
to: "now"
|
|
3713
|
+
});
|
|
3714
|
+
setOpen(false);
|
|
3715
|
+
}, [
|
|
3716
|
+
quickCount,
|
|
3717
|
+
quickUnit,
|
|
3718
|
+
commit
|
|
3719
|
+
]);
|
|
3720
|
+
const step = (0, react.useCallback)((direction) => {
|
|
3721
|
+
const next = (0, _reopt_ai_opt_charts.shiftTimeRange)(value, direction, { timeZone });
|
|
3722
|
+
if (next) commit(next);
|
|
3723
|
+
}, [
|
|
3724
|
+
value,
|
|
3725
|
+
timeZone,
|
|
3726
|
+
commit
|
|
3727
|
+
]);
|
|
3728
|
+
const zoom = (0, react.useCallback)((factor) => {
|
|
3729
|
+
const next = (0, _reopt_ai_opt_charts.zoomTimeRange)(value, factor, { timeZone });
|
|
3730
|
+
if (next) commit(next);
|
|
3731
|
+
}, [
|
|
3732
|
+
value,
|
|
3733
|
+
timeZone,
|
|
3734
|
+
commit
|
|
3735
|
+
]);
|
|
3736
|
+
useAutoRefresh({
|
|
3737
|
+
interval: (_refresh$interval = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval !== void 0 ? _refresh$interval : 0,
|
|
3738
|
+
paused: (_refresh$paused = refresh === null || refresh === void 0 ? void 0 : refresh.paused) !== null && _refresh$paused !== void 0 ? _refresh$paused : true,
|
|
3739
|
+
onRefresh
|
|
3850
3740
|
});
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
"aria-hidden": true,
|
|
3872
|
-
className: "size-1.5 shrink-0"
|
|
3873
|
-
}),
|
|
3874
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3875
|
-
className: "truncate",
|
|
3876
|
-
children: column.header
|
|
3877
|
-
}),
|
|
3878
|
-
active && (sort.direction === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {
|
|
3879
|
-
className: "size-3 shrink-0",
|
|
3880
|
-
"aria-hidden": true
|
|
3881
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {
|
|
3882
|
-
className: "size-3 shrink-0",
|
|
3883
|
-
"aria-hidden": true
|
|
3884
|
-
}))
|
|
3885
|
-
] });
|
|
3886
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3887
|
-
role: "columnheader",
|
|
3888
|
-
"aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
3889
|
-
className: require_text_truncate.cn("group/header flex items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority = column.priority) !== null && _column$priority !== void 0 ? _column$priority : "always"]),
|
|
3890
|
-
style: columnStyle(column),
|
|
3891
|
-
children: [sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3892
|
-
type: "button",
|
|
3893
|
-
className: "hover:text-text-primary flex cursor-pointer items-center gap-1.5 truncate",
|
|
3894
|
-
title: active && sort.direction === "desc" ? labels.sortAscending : labels.sortDescending,
|
|
3895
|
-
onClick: () => onSortChange({
|
|
3896
|
-
columnId: column.id,
|
|
3897
|
-
direction: active && sort.direction === "desc" ? "asc" : "desc"
|
|
3898
|
-
}),
|
|
3899
|
-
children: content
|
|
3900
|
-
}) : content, onColumnMove && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3901
|
-
className: require_text_truncate.cn("ml-auto flex shrink-0 items-center gap-0.5", "opacity-0 transition-opacity", "group-hover/header:opacity-100 focus-within:opacity-100"),
|
|
3902
|
-
children: [index > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3903
|
-
type: "button",
|
|
3904
|
-
className: "hover:text-text-primary cursor-pointer",
|
|
3905
|
-
"aria-label": labels.moveLeft(columnLabel),
|
|
3906
|
-
title: labels.moveLeft(columnLabel),
|
|
3907
|
-
onClick: () => onColumnMove(column.id, "left"),
|
|
3908
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
|
|
3909
|
-
className: "size-3",
|
|
3910
|
-
"aria-hidden": true
|
|
3911
|
-
})
|
|
3912
|
-
}), index < columns.length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3913
|
-
type: "button",
|
|
3914
|
-
className: "hover:text-text-primary cursor-pointer",
|
|
3915
|
-
"aria-label": labels.moveRight(columnLabel),
|
|
3916
|
-
title: labels.moveRight(columnLabel),
|
|
3917
|
-
onClick: () => onColumnMove(column.id, "right"),
|
|
3918
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
3919
|
-
className: "size-3",
|
|
3920
|
-
"aria-hidden": true
|
|
3921
|
-
})
|
|
3741
|
+
const refreshLabel = !refresh || refresh.paused || refresh.interval === 0 ? labels.autoRefreshOff : labels.every((0, _reopt_ai_opt_charts.formatDuration)(refresh.interval, locale));
|
|
3742
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3743
|
+
className: require_text_truncate.cn("bg-surface inline-flex items-center rounded-lg border", require_text_truncate.OPT_BORDER, className),
|
|
3744
|
+
role: "group",
|
|
3745
|
+
"aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.trigger,
|
|
3746
|
+
"data-opt-id": "3WJ8H",
|
|
3747
|
+
"data-opt-slug": "time-range-control.TimeRangeControl",
|
|
3748
|
+
children: [
|
|
3749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverRoot, {
|
|
3750
|
+
open,
|
|
3751
|
+
onOpenChange: setOpen,
|
|
3752
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverTrigger, {
|
|
3753
|
+
className: require_text_truncate.cn("text-text-primary h-10 gap-2", description.isInvalid && "text-danger-fg"),
|
|
3754
|
+
disabled,
|
|
3755
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Clock, {
|
|
3756
|
+
className: "text-text-tertiary h-4 w-4",
|
|
3757
|
+
"aria-hidden": "true"
|
|
3758
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3759
|
+
className: "truncate text-sm",
|
|
3760
|
+
children: description.text
|
|
3922
3761
|
})]
|
|
3923
|
-
})
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3762
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.PopoverContent, {
|
|
3763
|
+
className: "gap-group flex w-80 flex-col",
|
|
3764
|
+
children: [
|
|
3765
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3766
|
+
className: "gap-element flex flex-col",
|
|
3767
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3768
|
+
className: "text-text-primary text-xs font-medium",
|
|
3769
|
+
children: labels.quickSelect
|
|
3770
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3771
|
+
className: "flex items-end gap-2",
|
|
3772
|
+
children: [
|
|
3773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3774
|
+
"aria-label": labels.quickSelect,
|
|
3775
|
+
type: "number",
|
|
3776
|
+
min: 1,
|
|
3777
|
+
size: "sm",
|
|
3778
|
+
className: "w-20",
|
|
3779
|
+
value: quickCount,
|
|
3780
|
+
onChange: (event) => setQuickCount(event.target.value),
|
|
3781
|
+
onKeyDown: (event) => {
|
|
3782
|
+
if (event.key === "Enter") applyQuick();
|
|
3783
|
+
}
|
|
3784
|
+
}),
|
|
3785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.SelectRoot, {
|
|
3786
|
+
value: quickUnit,
|
|
3787
|
+
setValue: (next) => setQuickUnit(selectValue(next)),
|
|
3788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectTrigger, {
|
|
3789
|
+
minWidth: 110,
|
|
3790
|
+
"aria-label": labels.quickSelect
|
|
3791
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectPopover, { children: QUICK_UNITS.map((unit) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectItem, {
|
|
3792
|
+
value: unit,
|
|
3793
|
+
children: labels.units[unit]
|
|
3794
|
+
}, unit)) })]
|
|
3795
|
+
}),
|
|
3796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
3797
|
+
variant: "secondary",
|
|
3798
|
+
size: "sm",
|
|
3799
|
+
onClick: applyQuick,
|
|
3800
|
+
children: labels.apply
|
|
3801
|
+
})
|
|
3802
|
+
]
|
|
3803
|
+
})]
|
|
3804
|
+
}),
|
|
3805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3806
|
+
className: "gap-element flex flex-col",
|
|
3807
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3808
|
+
label: labels.start,
|
|
3809
|
+
size: "sm",
|
|
3810
|
+
value: editing.from,
|
|
3811
|
+
error: boundState(editing.from),
|
|
3812
|
+
hint: labels.startHint,
|
|
3813
|
+
onChange: (event) => setDraft(require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, editing), {}, { from: event.target.value })),
|
|
3814
|
+
onKeyDown: commitOnEnter
|
|
3815
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Input, {
|
|
3816
|
+
label: labels.end,
|
|
3817
|
+
size: "sm",
|
|
3818
|
+
value: editing.to,
|
|
3819
|
+
error: boundState(editing.to),
|
|
3820
|
+
onChange: (event) => setDraft(require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, editing), {}, { to: event.target.value })),
|
|
3821
|
+
onKeyDown: commitOnEnter
|
|
3822
|
+
})]
|
|
3823
|
+
}),
|
|
3824
|
+
presets && presets.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeList, {
|
|
3825
|
+
title: labels.commonlyUsed,
|
|
3826
|
+
items: presets,
|
|
3827
|
+
onPick: (range) => {
|
|
3828
|
+
commit(range);
|
|
3829
|
+
setOpen(false);
|
|
3830
|
+
}
|
|
3831
|
+
}),
|
|
3832
|
+
recentRanges && recentRanges.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeList, {
|
|
3833
|
+
title: labels.recentlyUsed,
|
|
3834
|
+
items: recentRanges.slice(0, 5),
|
|
3835
|
+
onPick: (range) => {
|
|
3836
|
+
commit(range);
|
|
3837
|
+
setOpen(false);
|
|
3838
|
+
}
|
|
3839
|
+
}),
|
|
3840
|
+
onRefreshChange && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3841
|
+
className: "gap-element flex flex-col",
|
|
3842
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3843
|
+
className: "text-text-primary text-xs font-medium",
|
|
3844
|
+
children: labels.autoRefresh
|
|
3845
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_text_truncate.SelectRoot, {
|
|
3846
|
+
value: String((refresh === null || refresh === void 0 ? void 0 : refresh.paused) ? 0 : (_refresh$interval2 = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval2 !== void 0 ? _refresh$interval2 : 0),
|
|
3847
|
+
setValue: (next) => {
|
|
3848
|
+
const interval = Number(selectValue(next));
|
|
3849
|
+
onRefreshChange({
|
|
3850
|
+
interval,
|
|
3851
|
+
paused: interval === 0
|
|
3852
|
+
});
|
|
3853
|
+
},
|
|
3854
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectTrigger, {
|
|
3855
|
+
minWidth: 140,
|
|
3856
|
+
"aria-label": labels.autoRefresh
|
|
3857
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectPopover, { children: REFRESH_INTERVALS.map((interval) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.SelectItem, {
|
|
3858
|
+
value: String(interval),
|
|
3859
|
+
children: interval === 0 ? labels.autoRefreshOff : labels.every((0, _reopt_ai_opt_charts.formatDuration)(interval, locale))
|
|
3860
|
+
}, interval)) })]
|
|
3861
|
+
})]
|
|
3862
|
+
})
|
|
3863
|
+
]
|
|
3864
|
+
})]
|
|
3865
|
+
}),
|
|
3866
|
+
showWindowButtons && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3867
|
+
className: require_text_truncate.cn("flex items-center border-l", "border border-border"),
|
|
3868
|
+
role: "group",
|
|
3869
|
+
"aria-label": labels.previous,
|
|
3953
3870
|
children: [
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3872
|
+
label: labels.previous,
|
|
3873
|
+
disabled: disabled || description.isInvalid,
|
|
3874
|
+
onClick: () => step(-1),
|
|
3875
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
|
|
3876
|
+
className: "h-4 w-4",
|
|
3877
|
+
"aria-hidden": "true"
|
|
3878
|
+
})
|
|
3958
3879
|
}),
|
|
3959
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
search: highlightTerms
|
|
3968
|
-
}) : shown
|
|
3969
|
-
}) : highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Highlight, {
|
|
3970
|
-
text,
|
|
3971
|
-
search: highlightTerms
|
|
3972
|
-
}) : text : EM_DASH
|
|
3880
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3881
|
+
label: labels.zoomIn,
|
|
3882
|
+
disabled: disabled || description.isInvalid,
|
|
3883
|
+
onClick: () => zoom(-zoomFactor),
|
|
3884
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, {
|
|
3885
|
+
className: "h-4 w-4",
|
|
3886
|
+
"aria-hidden": "true"
|
|
3887
|
+
})
|
|
3973
3888
|
}),
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3889
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3890
|
+
label: labels.zoomOut,
|
|
3891
|
+
disabled: disabled || description.isInvalid,
|
|
3892
|
+
onClick: () => zoom(zoomFactor),
|
|
3893
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, {
|
|
3894
|
+
className: "h-4 w-4",
|
|
3895
|
+
"aria-hidden": "true"
|
|
3896
|
+
})
|
|
3897
|
+
}),
|
|
3898
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
3899
|
+
label: labels.nextWindow,
|
|
3900
|
+
disabled: disabled || description.isInvalid,
|
|
3901
|
+
onClick: () => step(1),
|
|
3902
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
3903
|
+
className: "h-4 w-4",
|
|
3904
|
+
"aria-hidden": "true"
|
|
3905
|
+
})
|
|
3979
3906
|
})
|
|
3980
3907
|
]
|
|
3981
|
-
},
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
*/
|
|
3998
|
-
function CellActions({ field, value, onAction, labels }) {
|
|
3999
|
-
const button = "cursor-pointer rounded-sm p-0.5 text-text-tertiary hover:bg-bg-muted hover:text-text-primary";
|
|
4000
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4001
|
-
className: require_text_truncate.cn("bg-surface/90 ml-auto flex shrink-0 items-center gap-0.5 rounded pl-1", "opacity-0 transition-opacity", "group-hover/cell:opacity-100 focus-within:opacity-100"),
|
|
4002
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4003
|
-
type: "button",
|
|
4004
|
-
className: button,
|
|
4005
|
-
title: labels.filterFor(value),
|
|
4006
|
-
"aria-label": labels.filterFor(value),
|
|
4007
|
-
onClick: (event) => {
|
|
4008
|
-
event.stopPropagation();
|
|
4009
|
-
onAction({
|
|
4010
|
-
field,
|
|
4011
|
-
value,
|
|
4012
|
-
intent: "include"
|
|
4013
|
-
});
|
|
4014
|
-
},
|
|
4015
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
4016
|
-
className: "size-3",
|
|
4017
|
-
"aria-hidden": true
|
|
4018
|
-
})
|
|
4019
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4020
|
-
type: "button",
|
|
4021
|
-
className: button,
|
|
4022
|
-
title: labels.filterOut(value),
|
|
4023
|
-
"aria-label": labels.filterOut(value),
|
|
4024
|
-
onClick: (event) => {
|
|
4025
|
-
event.stopPropagation();
|
|
4026
|
-
onAction({
|
|
4027
|
-
field,
|
|
4028
|
-
value,
|
|
4029
|
-
intent: "exclude"
|
|
4030
|
-
});
|
|
4031
|
-
},
|
|
4032
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Minus, {
|
|
4033
|
-
className: "size-3",
|
|
4034
|
-
"aria-hidden": true
|
|
3908
|
+
}),
|
|
3909
|
+
(onRefresh || showUpdateButton) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3910
|
+
className: require_text_truncate.cn("border-l pl-2 pr-2", "border border-border"),
|
|
3911
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
3912
|
+
variant: hasChanges ? "primary" : "secondary",
|
|
3913
|
+
size: "sm",
|
|
3914
|
+
loading: isLoading,
|
|
3915
|
+
disabled: disabled || description.isInvalid,
|
|
3916
|
+
onClick: apply,
|
|
3917
|
+
leftIcon: hasChanges ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.RefreshCw, {
|
|
3918
|
+
className: "h-3.5 w-3.5",
|
|
3919
|
+
"aria-hidden": "true"
|
|
3920
|
+
}),
|
|
3921
|
+
title: refreshLabel,
|
|
3922
|
+
children: hasChanges ? labels.update : labels.refresh
|
|
3923
|
+
})
|
|
4035
3924
|
})
|
|
4036
|
-
|
|
4037
|
-
});
|
|
4038
|
-
}
|
|
4039
|
-
//#endregion
|
|
4040
|
-
//#region src/lib/query/query-ast.ts
|
|
4041
|
-
const EMPTY_QUERY = { clauses: [] };
|
|
4042
|
-
/** Whether a query constrains anything at all. */
|
|
4043
|
-
function isEmptyQuery(query) {
|
|
4044
|
-
return !query || query.clauses.length === 0;
|
|
4045
|
-
}
|
|
4046
|
-
function sameClause(a, b) {
|
|
4047
|
-
if (a.type !== b.type) return false;
|
|
4048
|
-
if (a.type === "term" && b.type === "term") return a.value === b.value && a.negated === b.negated;
|
|
4049
|
-
const fieldA = a;
|
|
4050
|
-
const fieldB = b;
|
|
4051
|
-
return fieldA.field === fieldB.field && fieldA.operator === fieldB.operator && fieldA.negated === fieldB.negated && fieldA.values.length === fieldB.values.length && fieldA.values.every((value, index) => value === fieldB.values[index]);
|
|
4052
|
-
}
|
|
4053
|
-
/** Add a clause unless an identical one is already there. */
|
|
4054
|
-
function addClause(query, clause) {
|
|
4055
|
-
if (query.clauses.some((existing) => sameClause(existing, clause))) return query;
|
|
4056
|
-
return { clauses: [...query.clauses, clause] };
|
|
4057
|
-
}
|
|
4058
|
-
/**
|
|
4059
|
-
* Constrain a field to a value — what a cell's "filter for this" action does.
|
|
4060
|
-
*
|
|
4061
|
-
* Applying the opposite of a constraint already present replaces it rather than
|
|
4062
|
-
* adding it: `status:200` then "filter out 200" should read as one changed
|
|
4063
|
-
* mind, not as a query that can never match.
|
|
4064
|
-
*/
|
|
4065
|
-
function withFieldValue(query, field, value, options = {}) {
|
|
4066
|
-
var _options$negated, _options$operator;
|
|
4067
|
-
const negated = (_options$negated = options.negated) !== null && _options$negated !== void 0 ? _options$negated : false;
|
|
4068
|
-
const operator = (_options$operator = options.operator) !== null && _options$operator !== void 0 ? _options$operator : "eq";
|
|
4069
|
-
return { clauses: [...query.clauses.filter((clause) => !(clause.type === "field" && clause.field === field && clause.operator === operator && clause.values.length === 1 && clause.values[0] === value)), {
|
|
4070
|
-
type: "field",
|
|
4071
|
-
field,
|
|
4072
|
-
operator,
|
|
4073
|
-
values: [value],
|
|
4074
|
-
negated
|
|
4075
|
-
}] };
|
|
4076
|
-
}
|
|
4077
|
-
/** Drop every clause naming `field`. */
|
|
4078
|
-
function withoutField(query, field) {
|
|
4079
|
-
return { clauses: query.clauses.filter((clause) => clause.type !== "field" || clause.field !== field) };
|
|
4080
|
-
}
|
|
4081
|
-
/** Drop the clause at `index`, for a removable chip. */
|
|
4082
|
-
function removeClause(query, index) {
|
|
4083
|
-
return { clauses: query.clauses.filter((_, at) => at !== index) };
|
|
4084
|
-
}
|
|
4085
|
-
/** Every field the query names, in the order they first appear. */
|
|
4086
|
-
function queryFields(query) {
|
|
4087
|
-
const seen = [];
|
|
4088
|
-
for (const clause of query.clauses) if (clause.type === "field" && !seen.includes(clause.field)) seen.push(clause.field);
|
|
4089
|
-
return seen;
|
|
4090
|
-
}
|
|
4091
|
-
/** The values a field is constrained to, if any — for showing a filter as active. */
|
|
4092
|
-
function fieldValues(query, field) {
|
|
4093
|
-
return query.clauses.flatMap((clause) => clause.type === "field" && clause.field === field && !clause.negated ? clause.values : []);
|
|
4094
|
-
}
|
|
4095
|
-
/**
|
|
4096
|
-
* Fold a cell action into the query.
|
|
4097
|
-
*
|
|
4098
|
-
* The pair of buttons on a log cell is the fastest path from "I noticed this
|
|
4099
|
-
* value" to a filtered view, and the reason it works is that the result is a
|
|
4100
|
-
* query the reader can then see, edit and share — not a hidden filter stack
|
|
4101
|
-
* with its own rules.
|
|
4102
|
-
*/
|
|
4103
|
-
function applyCellAction(query, action) {
|
|
4104
|
-
return withFieldValue(query, action.field, action.value, { negated: action.intent === "exclude" });
|
|
4105
|
-
}
|
|
4106
|
-
//#endregion
|
|
4107
|
-
//#region src/lib/query/query-parse.ts
|
|
4108
|
-
const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
4109
|
-
/**
|
|
4110
|
-
* Split on whitespace, but keep quoted runs and parenthesised groups whole.
|
|
4111
|
-
*
|
|
4112
|
-
* An unterminated quote or group swallows the rest of the line rather than
|
|
4113
|
-
* erroring: the reader is mid-word, and the alternative is the query flickering
|
|
4114
|
-
* between two different meanings as they type the closing character.
|
|
4115
|
-
*/
|
|
4116
|
-
function tokenize(input) {
|
|
4117
|
-
const tokens = [];
|
|
4118
|
-
let index = 0;
|
|
4119
|
-
while (index < input.length) {
|
|
4120
|
-
while (index < input.length && /\s/.test(input[index])) index += 1;
|
|
4121
|
-
if (index >= input.length) break;
|
|
4122
|
-
const start = index;
|
|
4123
|
-
let text = "";
|
|
4124
|
-
let quoted = false;
|
|
4125
|
-
let atTokenStart = true;
|
|
4126
|
-
let depth = 0;
|
|
4127
|
-
while (index < input.length) {
|
|
4128
|
-
const char = input[index];
|
|
4129
|
-
if (char === "\"") {
|
|
4130
|
-
if (atTokenStart) quoted = true;
|
|
4131
|
-
atTokenStart = false;
|
|
4132
|
-
index += 1;
|
|
4133
|
-
while (index < input.length && input[index] !== "\"") {
|
|
4134
|
-
text += input[index];
|
|
4135
|
-
index += 1;
|
|
4136
|
-
}
|
|
4137
|
-
index += 1;
|
|
4138
|
-
continue;
|
|
4139
|
-
}
|
|
4140
|
-
if (char === "(") depth += 1;
|
|
4141
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
4142
|
-
if (/\s/.test(char) && depth === 0) break;
|
|
4143
|
-
atTokenStart = false;
|
|
4144
|
-
text += char;
|
|
4145
|
-
index += 1;
|
|
4146
|
-
}
|
|
4147
|
-
if (text.length > 0 || quoted) tokens.push({
|
|
4148
|
-
text,
|
|
4149
|
-
at: start,
|
|
4150
|
-
quoted
|
|
4151
|
-
});
|
|
4152
|
-
}
|
|
4153
|
-
return tokens;
|
|
4154
|
-
}
|
|
4155
|
-
/** `(a or b)` → `["a", "b"]`; anything else is a single value. */
|
|
4156
|
-
function readValues(raw) {
|
|
4157
|
-
const trimmed = raw.trim();
|
|
4158
|
-
if (!trimmed.startsWith("(")) return [stripQuotes(trimmed)];
|
|
4159
|
-
return trimmed.replace(/^\(/, "").replace(/\)$/, "").split(/\s+or\s+/i).map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
|
|
3925
|
+
]
|
|
3926
|
+
});
|
|
4160
3927
|
}
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
return
|
|
3928
|
+
TimeRangeControl.displayName = "TimeRangeControl";
|
|
3929
|
+
function IconButton({ label, disabled, onClick, children }) {
|
|
3930
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
3931
|
+
variant: "ghost",
|
|
3932
|
+
size: "sm",
|
|
3933
|
+
"aria-label": label,
|
|
3934
|
+
title: label,
|
|
3935
|
+
disabled,
|
|
3936
|
+
onClick,
|
|
3937
|
+
children
|
|
3938
|
+
});
|
|
4164
3939
|
}
|
|
4165
|
-
|
|
3940
|
+
function RangeList({ title, items, onPick }) {
|
|
3941
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3942
|
+
className: "gap-element flex flex-col",
|
|
3943
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3944
|
+
className: "text-text-primary text-xs font-medium",
|
|
3945
|
+
children: title
|
|
3946
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3947
|
+
className: "grid grid-cols-2 gap-1",
|
|
3948
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3949
|
+
type: "button",
|
|
3950
|
+
className: require_text_truncate.cn("hover:bg-bg-subtle w-full cursor-pointer rounded px-2 py-1 text-left text-xs", require_text_truncate.OPT_TEXT_SECONDARY),
|
|
3951
|
+
onClick: () => onPick(item.range),
|
|
3952
|
+
children: item.label
|
|
3953
|
+
}) }, `${item.label}-${index}`))
|
|
3954
|
+
})]
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* The windows most analytics tools open with. Exported so a caller gets a
|
|
3959
|
+
* sensible list without inventing date math of their own.
|
|
3960
|
+
*/
|
|
3961
|
+
const DEFAULT_TIME_PRESETS = [
|
|
4166
3962
|
{
|
|
4167
|
-
|
|
4168
|
-
|
|
3963
|
+
label: "Last 15 minutes",
|
|
3964
|
+
range: {
|
|
3965
|
+
from: "now-15m",
|
|
3966
|
+
to: "now"
|
|
3967
|
+
}
|
|
4169
3968
|
},
|
|
4170
3969
|
{
|
|
4171
|
-
|
|
4172
|
-
|
|
3970
|
+
label: "Last hour",
|
|
3971
|
+
range: {
|
|
3972
|
+
from: "now-1h",
|
|
3973
|
+
to: "now"
|
|
3974
|
+
}
|
|
4173
3975
|
},
|
|
4174
3976
|
{
|
|
4175
|
-
|
|
4176
|
-
|
|
3977
|
+
label: "Last 24 hours",
|
|
3978
|
+
range: {
|
|
3979
|
+
from: "now-24h",
|
|
3980
|
+
to: "now"
|
|
3981
|
+
}
|
|
4177
3982
|
},
|
|
4178
3983
|
{
|
|
4179
|
-
|
|
4180
|
-
|
|
3984
|
+
label: "Last 7 days",
|
|
3985
|
+
range: {
|
|
3986
|
+
from: "now-7d",
|
|
3987
|
+
to: "now"
|
|
3988
|
+
}
|
|
4181
3989
|
},
|
|
4182
3990
|
{
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
/** Where a field operator starts in the token, or `-1`. */
|
|
4188
|
-
function findOperator(text) {
|
|
4189
|
-
let best = null;
|
|
4190
|
-
for (const { symbol, operator } of OPERATORS) {
|
|
4191
|
-
const index = text.indexOf(symbol);
|
|
4192
|
-
if (index <= 0) continue;
|
|
4193
|
-
if (!best || index < best.index || index === best.index && symbol.length > best.symbol.length) best = {
|
|
4194
|
-
index,
|
|
4195
|
-
symbol,
|
|
4196
|
-
operator
|
|
4197
|
-
};
|
|
4198
|
-
}
|
|
4199
|
-
return best;
|
|
4200
|
-
}
|
|
4201
|
-
/** Parse, keeping whatever could not be understood as a plain term. */
|
|
4202
|
-
function parseQueryWithDiagnostics(input) {
|
|
4203
|
-
const diagnostics = [];
|
|
4204
|
-
const clauses = [];
|
|
4205
|
-
let trailing = null;
|
|
4206
|
-
for (const token of tokenize(input)) {
|
|
4207
|
-
let text = token.text;
|
|
4208
|
-
let negated = false;
|
|
4209
|
-
if (!token.quoted && text.startsWith("-") && text.length > 1) {
|
|
4210
|
-
negated = true;
|
|
4211
|
-
text = text.slice(1);
|
|
4212
|
-
}
|
|
4213
|
-
const operator = token.quoted ? null : findOperator(text);
|
|
4214
|
-
if (!operator) {
|
|
4215
|
-
if (text.length > 0) clauses.push({
|
|
4216
|
-
type: "term",
|
|
4217
|
-
value: text,
|
|
4218
|
-
negated
|
|
4219
|
-
});
|
|
4220
|
-
continue;
|
|
4221
|
-
}
|
|
4222
|
-
const field = text.slice(0, operator.index);
|
|
4223
|
-
const rawValue = text.slice(operator.index + operator.symbol.length);
|
|
4224
|
-
if (!FIELD_NAME.test(field)) {
|
|
4225
|
-
clauses.push({
|
|
4226
|
-
type: "term",
|
|
4227
|
-
value: text,
|
|
4228
|
-
negated
|
|
4229
|
-
});
|
|
4230
|
-
continue;
|
|
4231
|
-
}
|
|
4232
|
-
if (rawValue === "") {
|
|
4233
|
-
trailing = {
|
|
4234
|
-
field,
|
|
4235
|
-
operator: operator.operator,
|
|
4236
|
-
value: ""
|
|
4237
|
-
};
|
|
4238
|
-
continue;
|
|
4239
|
-
}
|
|
4240
|
-
if (rawValue === "*" && operator.operator === "eq") {
|
|
4241
|
-
clauses.push({
|
|
4242
|
-
type: "field",
|
|
4243
|
-
field,
|
|
4244
|
-
operator: "exists",
|
|
4245
|
-
values: [],
|
|
4246
|
-
negated
|
|
4247
|
-
});
|
|
4248
|
-
continue;
|
|
3991
|
+
label: "Last 30 days",
|
|
3992
|
+
range: {
|
|
3993
|
+
from: "now-30d",
|
|
3994
|
+
to: "now"
|
|
4249
3995
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
negated
|
|
4257
|
-
});
|
|
4258
|
-
continue;
|
|
3996
|
+
},
|
|
3997
|
+
{
|
|
3998
|
+
label: "Last 90 days",
|
|
3999
|
+
range: {
|
|
4000
|
+
from: "now-90d",
|
|
4001
|
+
to: "now"
|
|
4259
4002
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
});
|
|
4267
|
-
continue;
|
|
4003
|
+
},
|
|
4004
|
+
{
|
|
4005
|
+
label: "Today",
|
|
4006
|
+
range: {
|
|
4007
|
+
from: "now/d",
|
|
4008
|
+
to: "now/d"
|
|
4268
4009
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
type: "field",
|
|
4276
|
-
field,
|
|
4277
|
-
operator: operator.operator,
|
|
4278
|
-
values: operator.operator === "eq" ? values : values.slice(0, 1),
|
|
4279
|
-
negated
|
|
4280
|
-
};
|
|
4281
|
-
clauses.push(clause);
|
|
4282
|
-
if (!rawValue.startsWith("(")) {
|
|
4283
|
-
var _values$;
|
|
4284
|
-
trailing = {
|
|
4285
|
-
field,
|
|
4286
|
-
operator: operator.operator,
|
|
4287
|
-
value: (_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : ""
|
|
4288
|
-
};
|
|
4010
|
+
},
|
|
4011
|
+
{
|
|
4012
|
+
label: "This week",
|
|
4013
|
+
range: {
|
|
4014
|
+
from: "now/w",
|
|
4015
|
+
to: "now/w"
|
|
4289
4016
|
}
|
|
4290
4017
|
}
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4018
|
+
];
|
|
4019
|
+
//#endregion
|
|
4020
|
+
//#region src/shells/time-series-panel.tsx
|
|
4021
|
+
const defaultLabels$2 = {
|
|
4022
|
+
back: "Back to previous range",
|
|
4023
|
+
legend: "Series"
|
|
4024
|
+
};
|
|
4025
|
+
/**
|
|
4026
|
+
* TimeSeriesPanel — a chart you can steer.
|
|
4027
|
+
*
|
|
4028
|
+
* The point of putting the control and the chart in one component is the
|
|
4029
|
+
* loop between them: drag across the plot and the window narrows to what you
|
|
4030
|
+
* dragged, which re-queries, which redraws the axis you just picked. That
|
|
4031
|
+
* only works if someone owns the step back out again — so zoom keeps a
|
|
4032
|
+
* history, and the header offers the way back rather than leaving the reader
|
|
4033
|
+
* to retype the range they came from.
|
|
4034
|
+
*
|
|
4035
|
+
* @example
|
|
4036
|
+
* ```tsx
|
|
4037
|
+
* const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
|
|
4038
|
+
*
|
|
4039
|
+
* <TimeSeriesPanel
|
|
4040
|
+
* title="Sessions"
|
|
4041
|
+
* data={points}
|
|
4042
|
+
* series={[{ dataKey: "sessions", name: "Sessions" }]}
|
|
4043
|
+
* range={range}
|
|
4044
|
+
* onRangeChange={setRange}
|
|
4045
|
+
* onRefresh={() => refetch()}
|
|
4046
|
+
* toggleableLegend
|
|
4047
|
+
* />
|
|
4048
|
+
* ```
|
|
4049
|
+
*/
|
|
4050
|
+
function TimeSeriesPanel({ title, description, actions, data, series, range, onRangeChange, granularity = "auto", timeZone, locale, showRangeControl = true, presets, recentRanges, refresh, onRefreshChange, onRefresh, toggleableLegend = false, annotations, isLoading = false, height = 280, className, chartProps, labels: customLabels, "aria-label": ariaLabel }) {
|
|
4051
|
+
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$2), customLabels);
|
|
4052
|
+
const titleId = (0, react.useId)();
|
|
4053
|
+
const [history, setHistory] = (0, react.useState)([]);
|
|
4054
|
+
const [hidden, setHidden] = (0, react.useState)([]);
|
|
4055
|
+
const resolved = (0, react.useMemo)(() => (0, _reopt_ai_opt_charts.resolveTimeRange)(range, { timeZone }), [range, timeZone]);
|
|
4056
|
+
const visibleSeries = (0, react.useMemo)(() => series.filter((def) => !hidden.includes(def.dataKey)), [series, hidden]);
|
|
4057
|
+
const handleRangeSelect = (0, react.useCallback)((selection) => {
|
|
4058
|
+
setHistory((current) => [...current, range].slice(-10));
|
|
4059
|
+
onRangeChange((0, _reopt_ai_opt_charts.toAbsoluteRange)(selection));
|
|
4060
|
+
}, [range, onRangeChange]);
|
|
4061
|
+
const goBack = (0, react.useCallback)(() => {
|
|
4062
|
+
setHistory((current) => {
|
|
4063
|
+
const previous = current[current.length - 1];
|
|
4064
|
+
if (previous) onRangeChange(previous);
|
|
4065
|
+
return current.slice(0, -1);
|
|
4066
|
+
});
|
|
4067
|
+
}, [onRangeChange]);
|
|
4068
|
+
const setRange = (0, react.useCallback)((next) => {
|
|
4069
|
+
setHistory([]);
|
|
4070
|
+
onRangeChange(next);
|
|
4071
|
+
}, [onRangeChange]);
|
|
4072
|
+
const toggleSeries = (0, react.useCallback)((dataKey) => {
|
|
4073
|
+
setHidden((current) => current.includes(dataKey) ? current.filter((key) => key !== dataKey) : [...current, dataKey]);
|
|
4074
|
+
}, []);
|
|
4075
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4076
|
+
className: require_text_truncate.cn(require_text_truncate.OPT_SURFACE, require_text_truncate.OPT_BORDER, "flex flex-col", className),
|
|
4077
|
+
"aria-label": ariaLabel,
|
|
4078
|
+
"aria-labelledby": ariaLabel == null && title != null ? titleId : void 0,
|
|
4079
|
+
"data-opt-id": "9EHHC",
|
|
4080
|
+
"data-opt-slug": "time-series-panel.TimeSeriesPanel",
|
|
4081
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
4082
|
+
className: "gap-group flex flex-wrap items-start justify-between px-5 pt-5 pb-3",
|
|
4083
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4084
|
+
className: "gap-element flex min-w-0 flex-col",
|
|
4085
|
+
children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
4086
|
+
id: titleId,
|
|
4087
|
+
className: "text-text-primary truncate text-base font-semibold",
|
|
4088
|
+
children: title
|
|
4089
|
+
}), description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4090
|
+
className: require_text_truncate.cn("text-text-secondary", "truncate text-sm"),
|
|
4091
|
+
children: description
|
|
4092
|
+
})]
|
|
4093
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4094
|
+
className: "gap-element flex flex-wrap items-center",
|
|
4095
|
+
children: [
|
|
4096
|
+
history.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Button, {
|
|
4097
|
+
variant: "ghost",
|
|
4098
|
+
size: "sm",
|
|
4099
|
+
onClick: goBack,
|
|
4100
|
+
leftIcon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, {
|
|
4101
|
+
className: "h-3.5 w-3.5",
|
|
4102
|
+
"aria-hidden": "true"
|
|
4103
|
+
}),
|
|
4104
|
+
children: labels.back
|
|
4105
|
+
}),
|
|
4106
|
+
actions,
|
|
4107
|
+
showRangeControl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeRangeControl, {
|
|
4108
|
+
value: range,
|
|
4109
|
+
onChange: setRange,
|
|
4110
|
+
presets,
|
|
4111
|
+
recentRanges,
|
|
4112
|
+
timeZone,
|
|
4113
|
+
locale,
|
|
4114
|
+
refresh,
|
|
4115
|
+
onRefreshChange,
|
|
4116
|
+
onRefresh,
|
|
4117
|
+
isLoading,
|
|
4118
|
+
labels: customLabels === null || customLabels === void 0 ? void 0 : customLabels.control
|
|
4119
|
+
})
|
|
4120
|
+
]
|
|
4121
|
+
})]
|
|
4122
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4123
|
+
className: "px-2 pb-4",
|
|
4124
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_reopt_ai_opt_charts.TimeSeriesChart, require_text_truncate._objectSpread2({
|
|
4125
|
+
data,
|
|
4126
|
+
series: visibleSeries,
|
|
4127
|
+
from: resolved === null || resolved === void 0 ? void 0 : resolved.from,
|
|
4128
|
+
to: resolved === null || resolved === void 0 ? void 0 : resolved.to,
|
|
4129
|
+
granularity,
|
|
4130
|
+
timeZone,
|
|
4131
|
+
locale,
|
|
4132
|
+
height,
|
|
4133
|
+
selectable: true,
|
|
4134
|
+
onRangeSelect: handleRangeSelect,
|
|
4135
|
+
annotations,
|
|
4136
|
+
showLegend: series.length > 1,
|
|
4137
|
+
legendOptions: toggleableLegend ? {
|
|
4138
|
+
onClick: toggleSeries,
|
|
4139
|
+
inactiveKeys: hidden
|
|
4140
|
+
} : void 0,
|
|
4141
|
+
"aria-label": typeof title === "string" ? `${title} time series` : ariaLabel
|
|
4142
|
+
}, chartProps))
|
|
4143
|
+
})]
|
|
4144
|
+
});
|
|
4300
4145
|
}
|
|
4146
|
+
TimeSeriesPanel.displayName = "TimeSeriesPanel";
|
|
4301
4147
|
//#endregion
|
|
4302
|
-
//#region src/
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4148
|
+
//#region src/shells/log-table.tsx
|
|
4149
|
+
const HIDE_BELOW = {
|
|
4150
|
+
always: "",
|
|
4151
|
+
sm: "hidden @[40rem]/log:flex",
|
|
4152
|
+
md: "hidden @[46rem]/log:flex",
|
|
4153
|
+
lg: "hidden @[58rem]/log:flex",
|
|
4154
|
+
xl: "hidden @[70rem]/log:flex"
|
|
4155
|
+
};
|
|
4156
|
+
const ROW_HEIGHT = {
|
|
4157
|
+
compact: 26,
|
|
4158
|
+
normal: 32,
|
|
4159
|
+
comfortable: 40
|
|
4160
|
+
};
|
|
4161
|
+
const DENSITY_TEXT = {
|
|
4162
|
+
compact: "text-[11px]",
|
|
4163
|
+
normal: "text-xs",
|
|
4164
|
+
comfortable: "text-sm"
|
|
4311
4165
|
};
|
|
4312
|
-
/** Quote a value that would otherwise re-parse as something else. */
|
|
4313
|
-
function quoteValue(value) {
|
|
4314
|
-
return /[\s"()]/.test(value) || value === "" ? `"${value.replace(/"/g, "")}"` : value;
|
|
4315
|
-
}
|
|
4316
|
-
/** One clause as text. */
|
|
4317
|
-
function formatClause(clause) {
|
|
4318
|
-
var _clause$values$2;
|
|
4319
|
-
const prefix = clause.negated ? "-" : "";
|
|
4320
|
-
if (clause.type === "term") return `${prefix}${quoteValue(clause.value)}`;
|
|
4321
|
-
if (clause.operator === "exists") return `${prefix}${clause.field}:*`;
|
|
4322
|
-
if (clause.operator === "contains") {
|
|
4323
|
-
var _clause$values$;
|
|
4324
|
-
return `${prefix}${clause.field}:*${(_clause$values$ = clause.values[0]) !== null && _clause$values$ !== void 0 ? _clause$values$ : ""}*`;
|
|
4325
|
-
}
|
|
4326
|
-
const symbol = OPERATOR_SYMBOL[clause.operator];
|
|
4327
|
-
if (clause.values.length > 1) return `${prefix}${clause.field}${symbol}(${clause.values.map(quoteValue).join(" or ")})`;
|
|
4328
|
-
return `${prefix}${clause.field}${symbol}${quoteValue((_clause$values$2 = clause.values[0]) !== null && _clause$values$2 !== void 0 ? _clause$values$2 : "")}`;
|
|
4329
|
-
}
|
|
4330
|
-
/** The whole query as text. */
|
|
4331
|
-
function formatQuery(query) {
|
|
4332
|
-
return query.clauses.map(formatClause).join(" ");
|
|
4333
|
-
}
|
|
4334
|
-
/** A short human label for a clause chip. */
|
|
4335
|
-
function describeClause(clause) {
|
|
4336
|
-
if (clause.type === "term") return clause.negated ? `not "${clause.value}"` : `"${clause.value}"`;
|
|
4337
|
-
if (clause.operator === "exists") return clause.negated ? `${clause.field} missing` : `${clause.field} present`;
|
|
4338
|
-
if (clause.operator === "contains") {
|
|
4339
|
-
var _clause$values$3;
|
|
4340
|
-
return `${clause.negated ? "not " : ""}${clause.field} ⊃ ${(_clause$values$3 = clause.values[0]) !== null && _clause$values$3 !== void 0 ? _clause$values$3 : ""}`;
|
|
4341
|
-
}
|
|
4342
|
-
const values = clause.values.join(", ");
|
|
4343
|
-
const symbol = OPERATOR_SYMBOL[clause.operator];
|
|
4344
|
-
return `${clause.negated ? "not " : ""}${clause.field} ${symbol === ":" ? "=" : symbol} ${values}`;
|
|
4345
|
-
}
|
|
4346
|
-
//#endregion
|
|
4347
|
-
//#region src/shells/query-bar.tsx
|
|
4348
4166
|
const defaultLabels$1 = {
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4167
|
+
listLabel: "Log",
|
|
4168
|
+
loadingMore: "Loading earlier entries…",
|
|
4169
|
+
endOfResults: "That is every entry in this range.",
|
|
4170
|
+
filterFor: (value) => `Filter for ${value}`,
|
|
4171
|
+
filterOut: (value) => `Filter out ${value}`,
|
|
4172
|
+
sortAscending: "Sort ascending",
|
|
4173
|
+
sortDescending: "Sort descending",
|
|
4174
|
+
moveLeft: (column) => `Move ${column} left`,
|
|
4175
|
+
moveRight: (column) => `Move ${column} right`
|
|
4356
4176
|
};
|
|
4357
|
-
|
|
4177
|
+
/** Rows rendered beyond the viewport on each side, so a fast scroll stays filled. */
|
|
4178
|
+
const OVERSCAN = 10;
|
|
4358
4179
|
/**
|
|
4359
|
-
*
|
|
4180
|
+
* A stable hue per value. Colour does real work in a log — the eye groups by it
|
|
4181
|
+
* long before it reads the text — so the same event name has to land on the
|
|
4182
|
+
* same hue on every render and in every session, which rules out an index into
|
|
4183
|
+
* a palette.
|
|
4184
|
+
*/
|
|
4185
|
+
function hueOf(value) {
|
|
4186
|
+
let hash = 0;
|
|
4187
|
+
for (let index = 0; index < value.length; index += 1) hash = hash * 31 + value.charCodeAt(index) | 0;
|
|
4188
|
+
return Math.abs(hash) % 360;
|
|
4189
|
+
}
|
|
4190
|
+
const EM_DASH = "—";
|
|
4191
|
+
/**
|
|
4192
|
+
* A column with nothing in it is a fact about the entry, not a value. Dimming
|
|
4193
|
+
* every absence the same way lets the eye skip the gaps while scanning, instead
|
|
4194
|
+
* of reading a dash as though it were data.
|
|
4195
|
+
*/
|
|
4196
|
+
const ABSENT = "text-text-tertiary/50";
|
|
4197
|
+
function columnStyle(column) {
|
|
4198
|
+
return column.width ? {
|
|
4199
|
+
width: column.width,
|
|
4200
|
+
flex: "0 0 auto"
|
|
4201
|
+
} : {
|
|
4202
|
+
flex: "1 1 0%",
|
|
4203
|
+
minWidth: 0
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* LogTable — a windowed table for entries that keep arriving.
|
|
4360
4208
|
*
|
|
4361
|
-
*
|
|
4362
|
-
*
|
|
4363
|
-
*
|
|
4364
|
-
*
|
|
4365
|
-
*
|
|
4209
|
+
* Written by hand rather than delegated to a data grid for the two things a log
|
|
4210
|
+
* needs that a grid does not: rows are inserted at the *top* while the reader
|
|
4211
|
+
* is partway down, which needs a scroll correction applied before paint, and
|
|
4212
|
+
* "am I at the newest row?" has to be answerable from the scroll container to
|
|
4213
|
+
* decide whether to follow the tail or hold position.
|
|
4366
4214
|
*
|
|
4367
|
-
*
|
|
4368
|
-
*
|
|
4369
|
-
*
|
|
4215
|
+
* What it takes from a data grid is the part readers actually reach for:
|
|
4216
|
+
* columns are data, so a caller can let people add, drop and reorder them; a
|
|
4217
|
+
* cell offers to filter for or against its own value; and density is a setting
|
|
4218
|
+
* rather than a constant.
|
|
4370
4219
|
*
|
|
4371
4220
|
* @example
|
|
4372
4221
|
* ```tsx
|
|
4373
|
-
* <
|
|
4374
|
-
*
|
|
4375
|
-
*
|
|
4376
|
-
*
|
|
4377
|
-
*
|
|
4378
|
-
*
|
|
4379
|
-
*
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
const
|
|
4387
|
-
const
|
|
4388
|
-
(0, react.
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
const nextText = suggestion.replace(text);
|
|
4404
|
-
setText(nextText);
|
|
4405
|
-
if (suggestion.kind === "value") commit(nextText);
|
|
4406
|
-
(_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
|
|
4407
|
-
setHighlighted(0);
|
|
4408
|
-
}, [text, commit]);
|
|
4409
|
-
const handleKeyDown = (event) => {
|
|
4410
|
-
if (suggestions.length > 0 && focused) {
|
|
4411
|
-
if (event.key === "ArrowDown") {
|
|
4412
|
-
event.preventDefault();
|
|
4413
|
-
setHighlighted((current) => (current + 1) % suggestions.length);
|
|
4414
|
-
return;
|
|
4415
|
-
}
|
|
4416
|
-
if (event.key === "ArrowUp") {
|
|
4417
|
-
event.preventDefault();
|
|
4418
|
-
setHighlighted((current) => (current - 1 + suggestions.length) % suggestions.length);
|
|
4419
|
-
return;
|
|
4420
|
-
}
|
|
4421
|
-
if (event.key === "Tab" || event.key === "Enter" && highlighted > 0) {
|
|
4422
|
-
event.preventDefault();
|
|
4423
|
-
applySuggestion(suggestions[highlighted]);
|
|
4424
|
-
return;
|
|
4222
|
+
* <LogTable
|
|
4223
|
+
* rows={rows}
|
|
4224
|
+
* getRowId={(row) => row.id}
|
|
4225
|
+
* columns={[
|
|
4226
|
+
* { id: "time", header: "Time", width: "9rem", mono: true, value: (r) => r.time },
|
|
4227
|
+
* { id: "name", header: "Event", width: "14rem", field: "name", marker: true, value: (r) => r.name },
|
|
4228
|
+
* { id: "path", header: "Path", field: "path", value: (r) => r.path },
|
|
4229
|
+
* ]}
|
|
4230
|
+
* onCellAction={(action) => setQuery(applyCellAction(query, action))}
|
|
4231
|
+
* />
|
|
4232
|
+
* ```
|
|
4233
|
+
*/
|
|
4234
|
+
function LogTableInner({ rows, columns, getRowId, selectedId = null, onSelect, onLoadMore, hasMore = false, loadingMore = false, prepending = false, onFollowChange, isNew, sort = null, onSortChange, onCellAction, highlightTerms, onColumnMove, density = "normal", showHeader = true, emptyState, labels: customLabels, className }, ref) {
|
|
4235
|
+
const labels = require_text_truncate._objectSpread2(require_text_truncate._objectSpread2({}, defaultLabels$1), customLabels);
|
|
4236
|
+
const rowHeight = ROW_HEIGHT[density];
|
|
4237
|
+
const scrollRef = (0, react.useRef)(null);
|
|
4238
|
+
const [scrollTop, setScrollTop] = (0, react.useState)(0);
|
|
4239
|
+
const [viewportHeight, setViewportHeight] = (0, react.useState)(0);
|
|
4240
|
+
const followingRef = (0, react.useRef)(true);
|
|
4241
|
+
const previousTopIdRef = (0, react.useRef)(null);
|
|
4242
|
+
const previousLengthRef = (0, react.useRef)(0);
|
|
4243
|
+
(0, react.useImperativeHandle)(ref, () => ({
|
|
4244
|
+
scrollToTop: () => {
|
|
4245
|
+
const container = scrollRef.current;
|
|
4246
|
+
if (!container) return;
|
|
4247
|
+
container.scrollTop = 0;
|
|
4248
|
+
setScrollTop(0);
|
|
4249
|
+
if (!followingRef.current) {
|
|
4250
|
+
followingRef.current = true;
|
|
4251
|
+
onFollowChange === null || onFollowChange === void 0 || onFollowChange(true);
|
|
4425
4252
|
}
|
|
4253
|
+
},
|
|
4254
|
+
revealRow: (id) => {
|
|
4255
|
+
const index = rows.findIndex((row) => getRowId(row) === id);
|
|
4256
|
+
const container = scrollRef.current;
|
|
4257
|
+
if (index === -1 || !container) return;
|
|
4258
|
+
const top = index * rowHeight;
|
|
4259
|
+
const bottom = top + rowHeight;
|
|
4260
|
+
if (top < container.scrollTop) container.scrollTop = top;
|
|
4261
|
+
else if (bottom > container.scrollTop + container.clientHeight) container.scrollTop = bottom - container.clientHeight;
|
|
4426
4262
|
}
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4263
|
+
}), [
|
|
4264
|
+
onFollowChange,
|
|
4265
|
+
rows,
|
|
4266
|
+
getRowId,
|
|
4267
|
+
rowHeight
|
|
4268
|
+
]);
|
|
4269
|
+
(0, react.useEffect)(() => {
|
|
4270
|
+
const container = scrollRef.current;
|
|
4271
|
+
if (!container) return;
|
|
4272
|
+
const observer = new ResizeObserver(() => setViewportHeight(container.clientHeight));
|
|
4273
|
+
observer.observe(container);
|
|
4274
|
+
setViewportHeight(container.clientHeight);
|
|
4275
|
+
return () => observer.disconnect();
|
|
4276
|
+
}, []);
|
|
4277
|
+
/**
|
|
4278
|
+
* Report whether the list is pinned to the newest row.
|
|
4279
|
+
*
|
|
4280
|
+
* Called from more than the scroll handler on purpose: the list can arrive at
|
|
4281
|
+
* the top without a scroll event — a reset, a jump to newest, a shorter
|
|
4282
|
+
* result set — and a stale "not following" would silently divert every live
|
|
4283
|
+
* row into a held-back pile.
|
|
4284
|
+
*/
|
|
4285
|
+
const syncFollowing = (0, react.useCallback)((container) => {
|
|
4286
|
+
const following = container.scrollTop <= rowHeight;
|
|
4287
|
+
if (following === followingRef.current) return;
|
|
4288
|
+
followingRef.current = following;
|
|
4289
|
+
onFollowChange === null || onFollowChange === void 0 || onFollowChange(following);
|
|
4290
|
+
}, [onFollowChange, rowHeight]);
|
|
4291
|
+
/**
|
|
4292
|
+
* Hold the reader's place when rows arrive above them.
|
|
4293
|
+
*
|
|
4294
|
+
* Runs before paint: by the time the browser draws, the scroll offset has
|
|
4295
|
+
* already absorbed the inserted height, so the content under the cursor never
|
|
4296
|
+
* moves. Skipped while following, where the whole point is to stay at the top.
|
|
4297
|
+
*/
|
|
4298
|
+
(0, react.useLayoutEffect)(() => {
|
|
4299
|
+
const container = scrollRef.current;
|
|
4300
|
+
const previousTopId = previousTopIdRef.current;
|
|
4301
|
+
previousTopIdRef.current = rows.length ? getRowId(rows[0]) : null;
|
|
4302
|
+
const previousLength = previousLengthRef.current;
|
|
4303
|
+
previousLengthRef.current = rows.length;
|
|
4304
|
+
if (!container) return;
|
|
4305
|
+
syncFollowing(container);
|
|
4306
|
+
if (!prepending || followingRef.current || !previousTopId) return;
|
|
4307
|
+
if (rows.length <= previousLength) return;
|
|
4308
|
+
const prepended = rows.findIndex((row) => getRowId(row) === previousTopId);
|
|
4309
|
+
if (prepended <= 0) return;
|
|
4310
|
+
container.scrollTop += prepended * rowHeight;
|
|
4311
|
+
setScrollTop(container.scrollTop);
|
|
4312
|
+
}, [
|
|
4313
|
+
rows,
|
|
4314
|
+
getRowId,
|
|
4315
|
+
prepending,
|
|
4316
|
+
rowHeight,
|
|
4317
|
+
syncFollowing
|
|
4318
|
+
]);
|
|
4319
|
+
/**
|
|
4320
|
+
* Page forward when the rows do not reach the bottom of the viewport.
|
|
4321
|
+
*
|
|
4322
|
+
* Without this the list only advances on a scroll event, so a tall screen —
|
|
4323
|
+
* or a filter matching few rows per page — stops at the first page with no
|
|
4324
|
+
* way to ask for more.
|
|
4325
|
+
*/
|
|
4326
|
+
(0, react.useEffect)(() => {
|
|
4327
|
+
const container = scrollRef.current;
|
|
4328
|
+
if (!container || !hasMore || loadingMore || !onLoadMore) return;
|
|
4329
|
+
if (container.scrollHeight <= container.clientHeight) onLoadMore();
|
|
4330
|
+
}, [
|
|
4331
|
+
hasMore,
|
|
4332
|
+
loadingMore,
|
|
4333
|
+
onLoadMore,
|
|
4334
|
+
rows.length,
|
|
4335
|
+
viewportHeight
|
|
4336
|
+
]);
|
|
4337
|
+
const handleScroll = (0, react.useCallback)((event) => {
|
|
4338
|
+
const container = event.currentTarget;
|
|
4339
|
+
setScrollTop(container.scrollTop);
|
|
4340
|
+
syncFollowing(container);
|
|
4341
|
+
const remaining = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
4342
|
+
if (hasMore && !loadingMore && remaining < container.clientHeight) onLoadMore === null || onLoadMore === void 0 || onLoadMore();
|
|
4343
|
+
}, [
|
|
4344
|
+
hasMore,
|
|
4345
|
+
loadingMore,
|
|
4346
|
+
onLoadMore,
|
|
4347
|
+
syncFollowing
|
|
4348
|
+
]);
|
|
4349
|
+
const totalHeight = rows.length * rowHeight;
|
|
4350
|
+
const firstVisible = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
|
|
4351
|
+
const visibleCount = Math.ceil((viewportHeight || 480) / rowHeight) + 20;
|
|
4352
|
+
const lastVisible = Math.min(rows.length, firstVisible + visibleCount);
|
|
4353
|
+
const visibleRows = (0, react.useMemo)(() => rows.slice(firstVisible, lastVisible), [
|
|
4354
|
+
rows,
|
|
4355
|
+
firstVisible,
|
|
4356
|
+
lastVisible
|
|
4357
|
+
]);
|
|
4439
4358
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4440
|
-
className: require_text_truncate.cn("
|
|
4441
|
-
"data-opt-id": "
|
|
4442
|
-
"data-opt-slug": "
|
|
4443
|
-
children: [
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4359
|
+
className: require_text_truncate.cn("@container/log flex min-h-0 flex-col", className),
|
|
4360
|
+
"data-opt-id": "3ZR82",
|
|
4361
|
+
"data-opt-slug": "log-table.LogTable",
|
|
4362
|
+
children: [showHeader && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogTableHeader, {
|
|
4363
|
+
columns,
|
|
4364
|
+
sort,
|
|
4365
|
+
onSortChange,
|
|
4366
|
+
onColumnMove,
|
|
4367
|
+
density,
|
|
4368
|
+
labels
|
|
4369
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4370
|
+
ref: scrollRef,
|
|
4371
|
+
onScroll: handleScroll,
|
|
4372
|
+
className: "relative min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
|
|
4373
|
+
role: "listbox",
|
|
4374
|
+
"aria-label": labels.listLabel,
|
|
4375
|
+
"aria-activedescendant": selectedId ? `log-row-${selectedId}` : void 0,
|
|
4376
|
+
tabIndex: 0,
|
|
4377
|
+
children: [
|
|
4378
|
+
rows.length === 0 ? emptyState : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4379
|
+
style: { height: totalHeight },
|
|
4380
|
+
className: "relative",
|
|
4381
|
+
children: visibleRows.map((row, index) => {
|
|
4382
|
+
var _isNew;
|
|
4383
|
+
const id = getRowId(row);
|
|
4384
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogTableRow, {
|
|
4385
|
+
id,
|
|
4386
|
+
row,
|
|
4387
|
+
columns,
|
|
4388
|
+
top: (firstVisible + index) * rowHeight,
|
|
4389
|
+
height: rowHeight,
|
|
4390
|
+
density,
|
|
4391
|
+
selected: id === selectedId,
|
|
4392
|
+
isNew: (_isNew = isNew === null || isNew === void 0 ? void 0 : isNew(row)) !== null && _isNew !== void 0 ? _isNew : false,
|
|
4393
|
+
onSelect,
|
|
4394
|
+
onCellAction,
|
|
4395
|
+
highlightTerms,
|
|
4396
|
+
labels
|
|
4397
|
+
}, id);
|
|
4398
|
+
})
|
|
4399
|
+
}),
|
|
4400
|
+
loadingMore && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4401
|
+
className: "gap-element text-text-tertiary flex items-center justify-center py-3 text-xs",
|
|
4402
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Spinner, {
|
|
4403
|
+
size: "sm",
|
|
4449
4404
|
"aria-hidden": true
|
|
4405
|
+
}), labels.loadingMore]
|
|
4406
|
+
}),
|
|
4407
|
+
!hasMore && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4408
|
+
className: require_text_truncate.cn("text-text-tertiary", "py-3 text-center text-xs"),
|
|
4409
|
+
children: labels.endOfResults
|
|
4410
|
+
})
|
|
4411
|
+
]
|
|
4412
|
+
})]
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
/**
|
|
4416
|
+
* LogTable — see {@link LogTableInner}.
|
|
4417
|
+
*
|
|
4418
|
+
* The cast preserves the row generic through `forwardRef`, which otherwise
|
|
4419
|
+
* widens it to its constraint and takes the column callbacks' types with it.
|
|
4420
|
+
*/
|
|
4421
|
+
const LogTable = (0, react.forwardRef)(LogTableInner);
|
|
4422
|
+
function LogTableHeader({ columns, sort, onSortChange, onColumnMove, density, labels }) {
|
|
4423
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4424
|
+
role: "row",
|
|
4425
|
+
className: require_text_truncate.cn("border-border-subtle text-text-tertiary flex shrink-0 items-center gap-3 border-b px-3", DENSITY_TEXT[density]),
|
|
4426
|
+
style: { height: ROW_HEIGHT[density] },
|
|
4427
|
+
children: columns.map((column, index) => {
|
|
4428
|
+
var _column$priority;
|
|
4429
|
+
const active = (sort === null || sort === void 0 ? void 0 : sort.columnId) === column.id;
|
|
4430
|
+
const sortable = column.sortable && onSortChange;
|
|
4431
|
+
const columnLabel = typeof column.header === "string" ? column.header : column.id;
|
|
4432
|
+
const content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4433
|
+
column.marker && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4434
|
+
"aria-hidden": true,
|
|
4435
|
+
className: "size-1.5 shrink-0"
|
|
4436
|
+
}),
|
|
4437
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4438
|
+
className: "truncate",
|
|
4439
|
+
children: column.header
|
|
4440
|
+
}),
|
|
4441
|
+
active && (sort.direction === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUp, {
|
|
4442
|
+
className: "size-3 shrink-0",
|
|
4443
|
+
"aria-hidden": true
|
|
4444
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, {
|
|
4445
|
+
className: "size-3 shrink-0",
|
|
4446
|
+
"aria-hidden": true
|
|
4447
|
+
}))
|
|
4448
|
+
] });
|
|
4449
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4450
|
+
role: "columnheader",
|
|
4451
|
+
"aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
4452
|
+
className: require_text_truncate.cn("group/header flex items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority = column.priority) !== null && _column$priority !== void 0 ? _column$priority : "always"]),
|
|
4453
|
+
style: columnStyle(column),
|
|
4454
|
+
children: [sortable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4455
|
+
type: "button",
|
|
4456
|
+
className: "hover:text-text-primary flex cursor-pointer items-center gap-1.5 truncate",
|
|
4457
|
+
title: active && sort.direction === "desc" ? labels.sortAscending : labels.sortDescending,
|
|
4458
|
+
onClick: () => onSortChange({
|
|
4459
|
+
columnId: column.id,
|
|
4460
|
+
direction: active && sort.direction === "desc" ? "asc" : "desc"
|
|
4450
4461
|
}),
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
"aria-expanded": showSuggestions,
|
|
4456
|
-
"aria-controls": "query-bar-suggestions",
|
|
4457
|
-
"aria-autocomplete": "list",
|
|
4458
|
-
"aria-label": labels.ariaLabel,
|
|
4459
|
-
placeholder: labels.placeholder,
|
|
4460
|
-
disabled,
|
|
4461
|
-
className: require_text_truncate.cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent font-mono text-sm outline-none focus-visible:outline-none"),
|
|
4462
|
-
value: text,
|
|
4463
|
-
onChange: (event) => {
|
|
4464
|
-
setText(event.target.value);
|
|
4465
|
-
setHighlighted(0);
|
|
4466
|
-
if (live) commit(event.target.value);
|
|
4467
|
-
},
|
|
4468
|
-
onFocus: () => setFocused(true),
|
|
4469
|
-
onBlur: () => {
|
|
4470
|
-
window.setTimeout(() => setFocused(false), 120);
|
|
4471
|
-
if (!live) commit(text);
|
|
4472
|
-
},
|
|
4473
|
-
onKeyDown: handleKeyDown
|
|
4474
|
-
}),
|
|
4475
|
-
text.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4462
|
+
children: content
|
|
4463
|
+
}) : content, onColumnMove && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4464
|
+
className: require_text_truncate.cn("ml-auto flex shrink-0 items-center gap-0.5", "opacity-0 transition-opacity", "group-hover/header:opacity-100 focus-within:opacity-100"),
|
|
4465
|
+
children: [index > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4476
4466
|
type: "button",
|
|
4477
|
-
"
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
onClick: () =>
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4467
|
+
className: "hover:text-text-primary cursor-pointer",
|
|
4468
|
+
"aria-label": labels.moveLeft(columnLabel),
|
|
4469
|
+
title: labels.moveLeft(columnLabel),
|
|
4470
|
+
onClick: () => onColumnMove(column.id, "left"),
|
|
4471
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, {
|
|
4472
|
+
className: "size-3",
|
|
4473
|
+
"aria-hidden": true
|
|
4474
|
+
})
|
|
4475
|
+
}), index < columns.length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4476
|
+
type: "button",
|
|
4477
|
+
className: "hover:text-text-primary cursor-pointer",
|
|
4478
|
+
"aria-label": labels.moveRight(columnLabel),
|
|
4479
|
+
title: labels.moveRight(columnLabel),
|
|
4480
|
+
onClick: () => onColumnMove(column.id, "right"),
|
|
4481
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
|
|
4482
|
+
className: "size-3",
|
|
4486
4483
|
"aria-hidden": true
|
|
4487
4484
|
})
|
|
4488
|
-
})
|
|
4489
|
-
]
|
|
4490
|
-
}),
|
|
4491
|
-
showSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4492
|
-
id: "query-bar-suggestions",
|
|
4493
|
-
role: "listbox",
|
|
4494
|
-
className: require_text_truncate.cn("bg-surface-overlay absolute top-10 z-40 max-h-64 w-full overflow-y-auto rounded-md border p-1 shadow-lg", "border border-border"),
|
|
4495
|
-
children: suggestions.map((suggestion, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4496
|
-
type: "button",
|
|
4497
|
-
role: "option",
|
|
4498
|
-
"aria-selected": index === highlighted,
|
|
4499
|
-
className: require_text_truncate.cn("flex w-full cursor-pointer items-center justify-between gap-3 rounded px-2 py-1 text-left text-sm", index === highlighted ? "bg-bg-subtle" : "hover:bg-bg-subtle"),
|
|
4500
|
-
onMouseEnter: () => setHighlighted(index),
|
|
4501
|
-
onMouseDown: (event) => event.preventDefault(),
|
|
4502
|
-
onClick: () => applySuggestion(suggestion),
|
|
4503
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4504
|
-
className: "text-text-primary truncate font-mono",
|
|
4505
|
-
children: suggestion.label
|
|
4506
|
-
}), suggestion.hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4507
|
-
className: "text-text-tertiary shrink-0 text-xs",
|
|
4508
|
-
children: suggestion.hint
|
|
4509
4485
|
})]
|
|
4510
|
-
})
|
|
4511
|
-
})
|
|
4512
|
-
|
|
4513
|
-
className: "flex flex-wrap items-center gap-1.5",
|
|
4514
|
-
children: value.clauses.map((clause, index) => {
|
|
4515
|
-
const label = describeClause(clause);
|
|
4516
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4517
|
-
className: require_text_truncate.cn("bg-bg-subtle text-text-secondary flex items-center gap-1 rounded px-1.5 py-0.5 text-xs", clause.negated && "text-danger-fg"),
|
|
4518
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4519
|
-
className: "truncate",
|
|
4520
|
-
children: label
|
|
4521
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4522
|
-
type: "button",
|
|
4523
|
-
"aria-label": labels.removeClause(label),
|
|
4524
|
-
className: "hover:text-text-primary cursor-pointer",
|
|
4525
|
-
onClick: () => {
|
|
4526
|
-
const next = removeClause(value, index);
|
|
4527
|
-
setText(formatQuery(next));
|
|
4528
|
-
committedRef.current = formatQuery(next);
|
|
4529
|
-
onChange(next);
|
|
4530
|
-
},
|
|
4531
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {
|
|
4532
|
-
className: "size-3",
|
|
4533
|
-
"aria-hidden": true
|
|
4534
|
-
})
|
|
4535
|
-
})]
|
|
4536
|
-
}, `${label}-${index}`);
|
|
4537
|
-
})
|
|
4538
|
-
}),
|
|
4539
|
-
parsed.diagnostics.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QueryDiagnostics, { diagnostics: parsed.diagnostics }),
|
|
4540
|
-
labels.syntaxHint && !showSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4541
|
-
className: "text-text-tertiary text-xs",
|
|
4542
|
-
children: labels.syntaxHint
|
|
4543
|
-
})
|
|
4544
|
-
]
|
|
4486
|
+
})]
|
|
4487
|
+
}, column.id);
|
|
4488
|
+
})
|
|
4545
4489
|
});
|
|
4546
4490
|
}
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
"
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4491
|
+
function LogTableRowImpl({ id, row, columns, top, height, density, selected, isNew, onSelect, onCellAction, highlightTerms, labels }) {
|
|
4492
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4493
|
+
role: "option",
|
|
4494
|
+
"aria-selected": selected,
|
|
4495
|
+
tabIndex: -1,
|
|
4496
|
+
id: `log-row-${id}`,
|
|
4497
|
+
"data-log-row-id": id,
|
|
4498
|
+
onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(id),
|
|
4499
|
+
onKeyDown: (event) => {
|
|
4500
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
4501
|
+
event.preventDefault();
|
|
4502
|
+
onSelect === null || onSelect === void 0 || onSelect(id);
|
|
4503
|
+
},
|
|
4504
|
+
style: {
|
|
4505
|
+
top,
|
|
4506
|
+
height
|
|
4507
|
+
},
|
|
4508
|
+
className: require_text_truncate.cn("border-border-subtle/60 absolute inset-x-0 flex items-center gap-3 border-b px-3", DENSITY_TEXT[density], onSelect && "cursor-pointer", "transition-colors duration-100", "hover:bg-surface-hover", selected && "bg-accent-subtle hover:bg-accent-subtle", isNew && !selected && "opt-log-row-enter"),
|
|
4509
|
+
children: columns.map((column) => {
|
|
4510
|
+
var _column$priority2;
|
|
4511
|
+
const raw = column.value(row);
|
|
4512
|
+
const text = raw === null || raw === void 0 || raw === "" ? "" : String(raw);
|
|
4513
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4514
|
+
className: require_text_truncate.cn("group/cell relative flex min-w-0 items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority2 = column.priority) !== null && _column$priority2 !== void 0 ? _column$priority2 : "always"]),
|
|
4515
|
+
style: columnStyle(column),
|
|
4516
|
+
children: [
|
|
4517
|
+
column.marker && text && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4518
|
+
"aria-hidden": true,
|
|
4519
|
+
className: "size-1.5 shrink-0 rounded-full",
|
|
4520
|
+
style: { backgroundColor: `hsl(${hueOf(text)} 65% 55%)` }
|
|
4521
|
+
}),
|
|
4522
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4523
|
+
className: require_text_truncate.cn("min-w-0 flex-1", !column.truncate && "truncate", column.mono && "font-mono", text ? "text-text-secondary" : ABSENT),
|
|
4524
|
+
title: column.truncate ? void 0 : text || void 0,
|
|
4525
|
+
children: column.render ? column.render(row) : text ? column.truncate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.TextTruncate, {
|
|
4526
|
+
text,
|
|
4527
|
+
truncation: column.truncate,
|
|
4528
|
+
children: (shown) => highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Highlight, {
|
|
4529
|
+
text: shown,
|
|
4530
|
+
search: highlightTerms
|
|
4531
|
+
}) : shown
|
|
4532
|
+
}) : highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_text_truncate.Highlight, {
|
|
4533
|
+
text,
|
|
4534
|
+
search: highlightTerms
|
|
4535
|
+
}) : text : EM_DASH
|
|
4536
|
+
}),
|
|
4537
|
+
onCellAction && column.field && text && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CellActions, {
|
|
4538
|
+
field: column.field,
|
|
4539
|
+
value: text,
|
|
4540
|
+
onAction: onCellAction,
|
|
4541
|
+
labels
|
|
4542
|
+
})
|
|
4543
|
+
]
|
|
4544
|
+
}, column.id);
|
|
4545
|
+
})
|
|
4560
4546
|
});
|
|
4561
4547
|
}
|
|
4562
4548
|
/**
|
|
4563
|
-
*
|
|
4549
|
+
* Memoized because a live log re-renders several times a second while almost
|
|
4550
|
+
* every visible row is unchanged.
|
|
4551
|
+
*/
|
|
4552
|
+
const LogTableRow = (0, react.memo)(LogTableRowImpl);
|
|
4553
|
+
/**
|
|
4554
|
+
* The two questions a reader has about a value they just spotted: show me only
|
|
4555
|
+
* these, and get these out of my way.
|
|
4564
4556
|
*
|
|
4565
|
-
*
|
|
4566
|
-
*
|
|
4567
|
-
*
|
|
4557
|
+
* Revealed on hover rather than always drawn — a log is scanned, and a pair of
|
|
4558
|
+
* buttons on every cell of every row is a wall of chrome. They stay in the tab
|
|
4559
|
+
* order regardless, since hover is not an interaction everyone has.
|
|
4568
4560
|
*/
|
|
4569
|
-
function
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4561
|
+
function CellActions({ field, value, onAction, labels }) {
|
|
4562
|
+
const button = "cursor-pointer rounded-sm p-0.5 text-text-tertiary hover:bg-bg-muted hover:text-text-primary";
|
|
4563
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4564
|
+
className: require_text_truncate.cn("bg-surface/90 ml-auto flex shrink-0 items-center gap-0.5 rounded pl-1", "opacity-0 transition-opacity", "group-hover/cell:opacity-100 focus-within:opacity-100"),
|
|
4565
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4566
|
+
type: "button",
|
|
4567
|
+
className: button,
|
|
4568
|
+
title: labels.filterFor(value),
|
|
4569
|
+
"aria-label": labels.filterFor(value),
|
|
4570
|
+
onClick: (event) => {
|
|
4571
|
+
event.stopPropagation();
|
|
4572
|
+
onAction({
|
|
4573
|
+
field,
|
|
4574
|
+
value,
|
|
4575
|
+
intent: "include"
|
|
4576
|
+
});
|
|
4577
|
+
},
|
|
4578
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, {
|
|
4579
|
+
className: "size-3",
|
|
4580
|
+
"aria-hidden": true
|
|
4581
|
+
})
|
|
4582
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4583
|
+
type: "button",
|
|
4584
|
+
className: button,
|
|
4585
|
+
title: labels.filterOut(value),
|
|
4586
|
+
"aria-label": labels.filterOut(value),
|
|
4587
|
+
onClick: (event) => {
|
|
4588
|
+
event.stopPropagation();
|
|
4589
|
+
onAction({
|
|
4590
|
+
field,
|
|
4591
|
+
value,
|
|
4592
|
+
intent: "exclude"
|
|
4593
|
+
});
|
|
4594
|
+
},
|
|
4595
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Minus, {
|
|
4596
|
+
className: "size-3",
|
|
4597
|
+
"aria-hidden": true
|
|
4598
|
+
})
|
|
4599
|
+
})]
|
|
4600
|
+
});
|
|
4598
4601
|
}
|
|
4599
4602
|
//#endregion
|
|
4600
4603
|
//#region src/shells/field-sidebar.tsx
|