Skip to content

Commit a8c1c7d

Browse files
committed
app/vmui: add Stats view mode for the graph (#34)
1 parent 6edb000 commit a8c1c7d

File tree

13 files changed

+448
-47
lines changed

13 files changed

+448
-47
lines changed

app/vmui/packages/vmui/src/api/logs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export const getLogsUrl = (server: string): string =>
33

44
export const getLogHitsUrl = (server: string): string =>
55
`${server}/select/logsql/hits`;
6+
7+
export const getStatsQueryRangeUrl = (server: string): string =>
8+
`${server}/select/logsql/stats_query_range`;

app/vmui/packages/vmui/src/api/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import { ReactNode } from "preact/compat";
33

44
export interface MetricBase {
55
group: number;
6-
metric: {
7-
[key: string]: string;
8-
};
6+
metric: Record<string, string>
97
}
108

119
export interface MetricResult extends MetricBase {

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsChart.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AlignedData } from "uplot";
55
import { TimeParams } from "../../../types";
66
import classNames from "classnames";
77
import { LogHits } from "../../../api/types";
8-
import { GraphOptions, GRAPH_STYLES } from "./types";
8+
import { GRAPH_QUERY_MODE, GRAPH_STYLES, GraphOptions } from "./types";
99
import BarHitsOptions from "./BarHitsOptions/BarHitsOptions";
1010
import BarHitsPlot from "./BarHitsPlot/BarHitsPlot";
1111
import { calculateTotalHits } from "../../../utils/logs";
@@ -23,21 +23,25 @@ interface Props {
2323
data: AlignedData;
2424
query?: string;
2525
period: TimeParams;
26-
durationMs?: number;
26+
durationMs?: number
27+
isOverview?: boolean;
2728
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
2829
onApplyFilter: (value: ExtraFilter) => void;
2930
}
3031

31-
const BarHitsChart: FC<Props> = ({ logHits, data: _data, query, period, setPeriod, onApplyFilter, durationMs }) => {
32+
const BarHitsChart: FC<Props> = ({ logHits, data: _data, query, period, setPeriod, onApplyFilter, durationMs, isOverview }) => {
3233
const { isMobile } = useDeviceDetect();
3334

3435
const [graphOptions, setGraphOptions] = useState<GraphOptions>({
3536
graphStyle: GRAPH_STYLES.BAR,
37+
queryMode: GRAPH_QUERY_MODE.hits,
3638
stacked: false,
3739
fill: false,
3840
hideChart: false,
3941
});
4042

43+
const isHitsMode = graphOptions.queryMode === GRAPH_QUERY_MODE.hits;
44+
4145
const totalHits = useMemo(() => calculateTotalHits(logHits), [logHits]);
4246

4347
const { extraParams } = useExtraFilters();
@@ -73,17 +77,21 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, query, period, setPerio
7377
limit={topHits}
7478
onChange={setTopHits}
7579
/> |
76-
<SelectLimit
77-
searchable
78-
label="Group by"
79-
limit={groupFieldHits}
80-
options={fieldNamesOptions}
81-
textNoOptions={"No fields found"}
82-
isLoading={loading}
83-
error={error ? String(error) : ""}
84-
onOpenSelect={handleOpenFields}
85-
onChange={setGroupFieldHits}
86-
/> |
80+
{isHitsMode && (
81+
<>
82+
<SelectLimit
83+
searchable
84+
label="Group by"
85+
limit={groupFieldHits}
86+
options={fieldNamesOptions}
87+
textNoOptions={"No fields found"}
88+
isLoading={loading}
89+
error={error ? String(error) : ""}
90+
onOpenSelect={handleOpenFields}
91+
onChange={setGroupFieldHits}
92+
/> |
93+
</>
94+
)}
8795
<p>Total: <b>{totalHits.toLocaleString("en-US")}</b> hits</p>
8896
{durationMs !== undefined && (
8997
<>
@@ -92,7 +100,10 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, query, period, setPerio
92100
</>
93101
)}
94102
</div>
95-
<BarHitsOptions onChange={setGraphOptions}/>
103+
<BarHitsOptions
104+
isOverview={isOverview}
105+
onChange={setGraphOptions}
106+
/>
96107
</div>
97108
{!graphOptions.hideChart && (
98109
<BarHitsPlot

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/BarHitsOptions/BarHitsOptions.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FC, useEffect, useMemo } from "preact/compat";
2-
import { GraphOptions, GRAPH_STYLES } from "../types";
2+
import { GraphOptions, GRAPH_STYLES, GRAPH_QUERY_MODE } from "../types";
33
import Switch from "../../../Main/Switch/Switch";
44
import "./style.scss";
55
import useStateSearchParams from "../../../../hooks/useStateSearchParams";
@@ -10,21 +10,32 @@ import Tooltip from "../../../Main/Tooltip/Tooltip";
1010
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
1111

1212
interface Props {
13+
isOverview?: boolean;
1314
onChange: (options: GraphOptions) => void;
1415
}
1516

16-
const BarHitsOptions: FC<Props> = ({ onChange }) => {
17+
const BarHitsOptions: FC<Props> = ({ isOverview, onChange }) => {
1718
const [searchParams, setSearchParams] = useSearchParams();
1819

20+
const [queryMode, setQueryMode] = useStateSearchParams(GRAPH_QUERY_MODE.hits, "graph_mode");
21+
const isStatsMode = queryMode === GRAPH_QUERY_MODE.stats;
1922
const [stacked, setStacked] = useStateSearchParams(false, "stacked");
2023
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
2124

2225
const options: GraphOptions = useMemo(() => ({
2326
graphStyle: GRAPH_STYLES.BAR,
27+
queryMode,
2428
stacked,
2529
fill: true,
2630
hideChart,
27-
}), [stacked, hideChart]);
31+
}), [stacked, hideChart, queryMode]);
32+
33+
const handleChangeMode = (val: boolean) => {
34+
const mode = val ? GRAPH_QUERY_MODE.stats : GRAPH_QUERY_MODE.hits;
35+
setQueryMode(mode);
36+
val ? searchParams.set("graph_mode", mode) : searchParams.delete("graph_mode");
37+
setSearchParams(searchParams);
38+
};
2839

2940
const handleChangeStacked = (val: boolean) => {
3041
setStacked(val);
@@ -47,6 +58,15 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
4758

4859
return (
4960
<div className="vm-bar-hits-options">
61+
{!isOverview && (
62+
<div className="vm-bar-hits-options-item">
63+
<Switch
64+
label="Stats view"
65+
value={isStatsMode}
66+
onChange={handleChangeMode}
67+
/>
68+
</div>
69+
)}
5070
<div className="vm-bar-hits-options-item">
5171
<Switch
5272
label={"Stacked"}

app/vmui/packages/vmui/src/components/Chart/BarHitsChart/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ export enum GRAPH_STYLES {
55
POINTS = "Points",
66
}
77

8+
export enum GRAPH_QUERY_MODE {
9+
hits = "hits",
10+
stats = "stats"
11+
}
12+
813
export interface GraphOptions {
914
graphStyle: GRAPH_STYLES;
15+
queryMode: GRAPH_QUERY_MODE;
1016
stacked: boolean;
1117
fill: boolean;
1218
hideChart: boolean;

app/vmui/packages/vmui/src/pages/OverviewPage/OverviewHits/OverviewHits.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const OverviewHits: FC = () => {
3636
return (
3737
<div>
3838
<HitsChart
39+
isOverview
3940
{...dataLogHits}
4041
query={query}
4142
period={period}

app/vmui/packages/vmui/src/pages/QueryPage/HitsChart/HitsChart.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ interface Props {
2222
period: TimeParams;
2323
error?: string;
2424
isLoading: boolean;
25+
isOverview?: boolean;
2526
onApplyFilter: (value: ExtraFilter) => void;
2627
}
2728

28-
const HitsChart: FC<Props> = ({ query, logHits, period, error, isLoading, onApplyFilter, durationMs }) => {
29+
const HitsChart: FC<Props> = ({ query, logHits, durationMs, period, error, isLoading, isOverview, onApplyFilter }) => {
2930
const { isMobile } = useDeviceDetect();
3031
const timeDispatch = useTimeDispatch();
3132
const [searchParams] = useSearchParams();
@@ -109,12 +110,13 @@ const HitsChart: FC<Props> = ({ query, logHits, period, error, isLoading, onAppl
109110

110111
{error && noDataMessage && (
111112
<div className="vm-query-page-chart__empty">
112-
<Alert variant="error">{error}</Alert>
113+
<Alert variant="error"><pre>{error}</pre></Alert>
113114
</div>
114115
)}
115116

116117
{data && (
117118
<BarHitsChart
119+
isOverview={isOverview}
118120
logHits={logHits}
119121
durationMs={durationMs}
120122
query={query}

app/vmui/packages/vmui/src/pages/QueryPage/QueryPage.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useLimitGuard } from "./LimitController/useLimitGuard";
2525
import LimitConfirmModal from "./LimitController/LimitConfirmModal";
2626
import { useFetchQueryTime } from "./hooks/useFetchQueryTime";
2727
import { getOverrideValue } from "../../components/Configurators/GlobalSettings/QueryTimeOverride/QueryTimeOverride";
28+
import { GRAPH_QUERY_MODE } from "../../components/Chart/BarHitsChart/types";
2829

2930
const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
3031
const defaultLimit = isNaN(storageLimit) ? LOGS_DEFAULT_LIMIT : storageLimit;
@@ -49,6 +50,9 @@ const QueryPage: FC = () => {
4950
const hideLogs = useMemo(() => Boolean(searchParams.get("hide_logs")), [searchParams]);
5051
const prevHideLogs = usePrevious(hideLogs);
5152

53+
const [graphQueryMode] = useStateSearchParams(GRAPH_QUERY_MODE.hits, "graph_mode");
54+
const prevGraphMode = usePrevious(graphQueryMode);
55+
5256
const [limit, setLimit] = useStateSearchParams(defaultLimit, LOGS_URL_PARAMS.LIMIT);
5357
const [query, setQuery] = useStateSearchParams("*", "query");
5458

@@ -88,7 +92,7 @@ const QueryPage: FC = () => {
8892
}
8993

9094
if (flags.hits) {
91-
await fetchLogHits({ period, field: groupFieldHits, fieldsLimit: topHits });
95+
await fetchLogHits({ period, field: groupFieldHits, fieldsLimit: topHits, queryMode: graphQueryMode });
9296
}
9397
};
9498

@@ -173,12 +177,24 @@ const QueryPage: FC = () => {
173177
const topChanged = prevTopHits && (topHits !== prevTopHits);
174178
const groupChanged = prevGroupFieldHits && (groupFieldHits !== prevGroupFieldHits);
175179
const becameVisible = prevHideChart && !hideChart;
180+
const queryModeChanged = prevGraphMode && (graphQueryMode !== prevGraphMode);
176181

177-
if (!(topChanged || groupChanged || becameVisible)) return;
182+
if (!(topChanged || groupChanged || becameVisible || queryModeChanged)) return;
178183

179184
dataLogHits.abortController.abort?.();
180-
fetchLogHits({ period, field: groupFieldHits, fieldsLimit: topHits });
181-
}, [hideChart, prevHideChart, period, groupFieldHits, prevGroupFieldHits, topHits, prevTopHits, fetchLogHits]);
185+
fetchLogHits({ period, field: groupFieldHits, fieldsLimit: topHits, queryMode: graphQueryMode });
186+
}, [
187+
hideChart,
188+
prevHideChart,
189+
period,
190+
groupFieldHits,
191+
prevGroupFieldHits,
192+
topHits,
193+
prevTopHits,
194+
graphQueryMode,
195+
prevGraphMode,
196+
fetchLogHits,
197+
]);
182198

183199
useEffect(() => {
184200
if (hideLogs || !prevHideLogs) return;

app/vmui/packages/vmui/src/pages/QueryPage/hooks/useFetchLogHits.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useCallback, useMemo, useRef, useState } from "preact/compat";
2-
import { getLogHitsUrl } from "../../../api/logs";
2+
import { getLogHitsUrl, getStatsQueryRangeUrl } from "../../../api/logs";
33
import { ErrorTypes, TimeParams } from "../../../types";
44
import { LogHits } from "../../../api/types";
55
import { getHitsTimeParams } from "../../../utils/logs";
@@ -8,13 +8,22 @@ import { isEmptyObject } from "../../../utils/object";
88
import { useTenant } from "../../../hooks/useTenant";
99
import { useSearchParams } from "react-router-dom";
1010
import { useAppState } from "../../../state/common/StateContext";
11+
import { GRAPH_QUERY_MODE } from "../../../components/Chart/BarHitsChart/types";
12+
import useProcessStatsQueryRange from "./useProcessStatsQueryRange";
13+
14+
15+
16+
type ResponseHits = {
17+
hits: LogHits[];
18+
}
1119

1220
interface FetchHitsParams {
1321
query?: string;
1422
period: TimeParams;
1523
extraParams?: URLSearchParams;
1624
field?: string;
1725
fieldsLimit?: number;
26+
queryMode?: GRAPH_QUERY_MODE
1827
}
1928

2029
interface OptionsParams extends FetchHitsParams {
@@ -27,14 +36,23 @@ export const useFetchLogHits = (defaultQuery = "*") => {
2736
const [searchParams] = useSearchParams();
2837

2938
const [logHits, setLogHits] = useState<LogHits[]>([]);
30-
const [isLoading, setIsLoading] = useState<{[key: number]: boolean;}>([]);
39+
const [isLoading, setIsLoading] = useState<{ [key: number]: boolean; }>([]);
3140
const [error, setError] = useState<ErrorTypes | string>();
3241
const [durationMs, setDurationMs] = useState<number | undefined>();
3342
const abortControllerRef = useRef(new AbortController());
3443

44+
const processStatsQueryRange = useProcessStatsQueryRange({ setLogHits, setError });
45+
3546
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
3647

37-
const url = useMemo(() => getLogHitsUrl(serverUrl), [serverUrl]);
48+
const getUrl = useCallback((queryMode: GRAPH_QUERY_MODE) => {
49+
switch (queryMode) {
50+
case GRAPH_QUERY_MODE.hits:
51+
return getLogHitsUrl(serverUrl);
52+
case GRAPH_QUERY_MODE.stats:
53+
return getStatsQueryRangeUrl(serverUrl);
54+
}
55+
}, [serverUrl]);
3856

3957
const getOptions = ({ query = defaultQuery, period, extraParams, signal, fieldsLimit, field }: OptionsParams) => {
4058
const { start, end, step, offset } = getHitsTimeParams(period);
@@ -64,7 +82,24 @@ export const useFetchLogHits = (defaultQuery = "*") => {
6482
};
6583
};
6684

85+
const processHits = (data: ResponseHits) => {
86+
const hitsRaw = data?.hits as LogHits[];
87+
88+
if (!hitsRaw) {
89+
const error = "Error: No 'hits' field in response";
90+
setError(error);
91+
return [];
92+
}
93+
94+
const hits = hitsRaw.map(markIsOther).sort(sortHits);
95+
setLogHits(hits);
96+
97+
return hits;
98+
};
99+
67100
const fetchLogHits = useCallback(async (params: FetchHitsParams) => {
101+
const queryMode = params.queryMode || GRAPH_QUERY_MODE.hits;
102+
68103
abortControllerRef.current.abort();
69104
abortControllerRef.current = new AbortController();
70105
const { signal } = abortControllerRef.current;
@@ -75,36 +110,45 @@ export const useFetchLogHits = (defaultQuery = "*") => {
75110

76111
try {
77112
const options = getOptions({ ...params, signal });
113+
const url = getUrl(queryMode);
78114
const response = await fetch(url, options);
79115

80116
const duration = response.headers.get("vl-request-duration-seconds");
81117
setDurationMs(duration ? Number(duration) * 1000 : undefined);
82118

83119
if (!response.ok || !response.body) {
84120
const text = await response.text();
85-
setError(text);
121+
try {
122+
setError(JSON.stringify(JSON.parse(text), null, 2));
123+
} catch (_e) {
124+
setError(text);
125+
}
86126
setLogHits([]);
87127
setIsLoading(prev => ({ ...prev, [id]: false }));
88128
return;
89129
}
90130

91131
const data = await response.json();
92-
const hits = data?.hits as LogHits[];
93-
if (!hits) {
94-
const error = "Error: No 'hits' field in response";
95-
setError(error);
132+
133+
switch (queryMode) {
134+
case GRAPH_QUERY_MODE.hits:
135+
return processHits(data);
136+
case GRAPH_QUERY_MODE.stats: {
137+
const fieldsLimit = +(options.body.get("fields_limit") || LOGS_LIMIT_HITS);
138+
return processStatsQueryRange(data, fieldsLimit);
139+
}
96140
}
97141

98-
setLogHits(hits.map(markIsOther).sort(sortHits));
99142
} catch (e) {
100143
if (e instanceof Error && e.name !== "AbortError") {
101144
setError(String(e));
102145
console.error(e);
103146
setLogHits([]);
104147
}
148+
} finally {
149+
setIsLoading(prev => ({ ...prev, [id]: false }));
105150
}
106-
setIsLoading(prev => ({ ...prev, [id]: false }));
107-
}, [url, defaultQuery, tenant]);
151+
}, [getUrl, defaultQuery, tenant]);
108152

109153
useEffect(() => {
110154
return () => {

0 commit comments

Comments
 (0)