vmui: add the ability to cancel running queries (#7204)

### Describe Your Changes

- Added functionality to cancel running queries on the Explore Logs and
Query pages.
- The loader was changed from a spinner to a top bar within the block.
This still indicates loading, but solves the issue of the spinner
"flickering," especially during graph dragging.

Related issue: #7097


https://github.com/user-attachments/assets/98e59aeb-905b-4b9d-bbb2-688223b22a82

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/contributing/).
This commit is contained in:
Yury Molodov 2024-10-15 14:48:40 +02:00 committed by GitHub
parent a8d8987825
commit 6c9772b101
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 145 additions and 27 deletions

View file

@ -553,3 +553,20 @@ export const SearchIcon = () => (
></path> ></path>
</svg> </svg>
); );
export const SpinnerIcon = () => (
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"
>
<animateTransform
attributeName="transform"
dur="0.75s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
</svg>
);

View file

@ -0,0 +1,13 @@
import React, { FC } from "preact/compat";
import "./style.scss";
const LineLoader: FC = () => {
return (
<div className="vm-line-loader">
<div className="vm-line-loader__background"></div>
<div className="vm-line-loader__line"></div>
</div>
);
};
export default LineLoader;

View file

@ -0,0 +1,39 @@
@use "src/styles/variables" as *;
.vm-line-loader {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
z-index: 2;
overflow: hidden;
&__background {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: $color-text;
opacity: 0.1;
}
&__line {
position: absolute;
width: 10%;
height: 100%;
background-color: $color-primary;
animation: slide 2s infinite linear;
opacity: 0.8;
}
}
@keyframes slide {
0% {
left: 0;
}
100% {
left: 100%;
}
}

View file

@ -34,7 +34,8 @@ interface FetchQueryReturn {
queryStats: QueryStats[], queryStats: QueryStats[],
warning?: string, warning?: string,
traces?: Trace[], traces?: Trace[],
isHistogram: boolean isHistogram: boolean,
abortFetch: () => void
} }
interface FetchDataParams { interface FetchDataParams {
@ -160,6 +161,7 @@ export const useFetchQuery = ({
const error = e as Error; const error = e as Error;
if (error.name === "AbortError") { if (error.name === "AbortError") {
// Aborts are expected, don't show an error for them. // Aborts are expected, don't show an error for them.
setIsLoading(false);
return; return;
} }
const helperText = "Please check your serverURL settings and confirm server availability."; const helperText = "Please check your serverURL settings and confirm server availability.";
@ -197,6 +199,13 @@ export const useFetchQuery = ({
}, },
[serverUrl, period, displayType, customStep, hideQuery]); [serverUrl, period, displayType, customStep, hideQuery]);
const abortFetch = useCallback(() => {
fetchQueue.map(f => f.abort());
setFetchQueue([]);
setGraphData([]);
setLiveData([]);
}, [fetchQueue]);
const [prevUrl, setPrevUrl] = useState<string[]>([]); const [prevUrl, setPrevUrl] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
@ -238,6 +247,7 @@ export const useFetchQuery = ({
queryStats, queryStats,
warning, warning,
traces, traces,
isHistogram isHistogram,
abortFetch,
}; };
}; };

View file

@ -10,6 +10,7 @@ import {
PlayIcon, PlayIcon,
PlusIcon, PlusIcon,
Prettify, Prettify,
SpinnerIcon,
VisibilityIcon, VisibilityIcon,
VisibilityOffIcon VisibilityOffIcon
} from "../../../components/Main/Icons"; } from "../../../components/Main/Icons";
@ -30,8 +31,10 @@ export interface QueryConfiguratorProps {
setQueryErrors: Dispatch<SetStateAction<string[]>>; setQueryErrors: Dispatch<SetStateAction<string[]>>;
setHideError: Dispatch<SetStateAction<boolean>>; setHideError: Dispatch<SetStateAction<boolean>>;
stats: QueryStats[]; stats: QueryStats[];
isLoading?: boolean;
onHideQuery?: (queries: number[]) => void onHideQuery?: (queries: number[]) => void
onRunQuery: () => void; onRunQuery: () => void;
abortFetch?: () => void;
hideButtons?: { hideButtons?: {
addQuery?: boolean; addQuery?: boolean;
prettify?: boolean; prettify?: boolean;
@ -46,8 +49,10 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
setQueryErrors, setQueryErrors,
setHideError, setHideError,
stats, stats,
isLoading,
onHideQuery, onHideQuery,
onRunQuery, onRunQuery,
abortFetch,
hideButtons hideButtons
}) => { }) => {
@ -84,6 +89,10 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
}; };
const handleRunQuery = () => { const handleRunQuery = () => {
if (isLoading) {
abortFetch && abortFetch();
return;
}
updateHistory(); updateHistory();
queryDispatch({ type: "SET_QUERY", payload: stateQuery }); queryDispatch({ type: "SET_QUERY", payload: stateQuery });
timeDispatch({ type: "RUN_QUERY" }); timeDispatch({ type: "RUN_QUERY" });
@ -271,9 +280,9 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
<Button <Button
variant="contained" variant="contained"
onClick={handleRunQuery} onClick={handleRunQuery}
startIcon={<PlayIcon/>} startIcon={isLoading ? <SpinnerIcon/> : <PlayIcon/>}
> >
{isMobile ? "Execute" : "Execute Query"} {`${isLoading ? "Cancel" : "Execute"} ${isMobile ? "" : "Query"}`}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -3,7 +3,7 @@ import QueryConfigurator from "./QueryConfigurator/QueryConfigurator";
import { useFetchQuery } from "../../hooks/useFetchQuery"; import { useFetchQuery } from "../../hooks/useFetchQuery";
import { DisplayTypeSwitch } from "./DisplayTypeSwitch"; import { DisplayTypeSwitch } from "./DisplayTypeSwitch";
import { useGraphDispatch, useGraphState } from "../../state/graph/GraphStateContext"; import { useGraphDispatch, useGraphState } from "../../state/graph/GraphStateContext";
import Spinner from "../../components/Main/Spinner/Spinner"; import LineLoader from "../../components/Main/LineLoader/LineLoader";
import { useCustomPanelState } from "../../state/customPanel/CustomPanelStateContext"; import { useCustomPanelState } from "../../state/customPanel/CustomPanelStateContext";
import { useQueryState } from "../../state/query/QueryStateContext"; import { useQueryState } from "../../state/query/QueryStateContext";
import { useSetQueryParams } from "./hooks/useSetQueryParams"; import { useSetQueryParams } from "./hooks/useSetQueryParams";
@ -45,7 +45,8 @@ const CustomPanel: FC = () => {
queryStats, queryStats,
warning, warning,
traces, traces,
isHistogram isHistogram,
abortFetch,
} = useFetchQuery({ } = useFetchQuery({
visible: true, visible: true,
customStep, customStep,
@ -80,14 +81,15 @@ const CustomPanel: FC = () => {
setQueryErrors={setQueryErrors} setQueryErrors={setQueryErrors}
setHideError={setHideError} setHideError={setHideError}
stats={queryStats} stats={queryStats}
isLoading={isLoading}
onHideQuery={handleHideQuery} onHideQuery={handleHideQuery}
onRunQuery={handleRunQuery} onRunQuery={handleRunQuery}
abortFetch={abortFetch}
/> />
<CustomPanelTraces <CustomPanelTraces
traces={traces} traces={traces}
displayType={displayType} displayType={displayType}
/> />
{isLoading && <Spinner />}
{showError && <Alert variant="error">{error}</Alert>} {showError && <Alert variant="error">{error}</Alert>}
{showInstantQueryTip && <Alert variant="info"><InstantQueryTip/></Alert>} {showInstantQueryTip && <Alert variant="info"><InstantQueryTip/></Alert>}
{warning && ( {warning && (
@ -105,6 +107,7 @@ const CustomPanel: FC = () => {
"vm-block_mobile": isMobile, "vm-block_mobile": isMobile,
})} })}
> >
{isLoading && <LineLoader />}
<div <div
className="vm-custom-panel-body-header" className="vm-custom-panel-body-header"
ref={controlsRef} ref={controlsRef}

View file

@ -4,7 +4,6 @@ import useStateSearchParams from "../../hooks/useStateSearchParams";
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject"; import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
import { useFetchLogs } from "./hooks/useFetchLogs"; import { useFetchLogs } from "./hooks/useFetchLogs";
import { useAppState } from "../../state/common/StateContext"; import { useAppState } from "../../state/common/StateContext";
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert"; import Alert from "../../components/Main/Alert/Alert";
import ExploreLogsHeader from "./ExploreLogsHeader/ExploreLogsHeader"; import ExploreLogsHeader from "./ExploreLogsHeader/ExploreLogsHeader";
import "./style.scss"; import "./style.scss";
@ -30,7 +29,7 @@ const ExploreLogs: FC = () => {
const [period, setPeriod] = useState<TimeParams>(periodState); const [period, setPeriod] = useState<TimeParams>(periodState);
const [queryError, setQueryError] = useState<ErrorTypes | string>(""); const [queryError, setQueryError] = useState<ErrorTypes | string>("");
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit); const { logs, isLoading, error, fetchLogs, abortController } = useFetchLogs(serverUrl, query, limit);
const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query); const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query);
const getPeriod = useCallback(() => { const getPeriod = useCallback(() => {
@ -70,10 +69,15 @@ const ExploreLogs: FC = () => {
setQuery(prev => `_stream: ${val === "other" ? "{}" : val} AND (${prev})`); setQuery(prev => `_stream: ${val === "other" ? "{}" : val} AND (${prev})`);
}; };
const handleUpdateQuery = () => { const handleUpdateQuery = useCallback(() => {
if (isLoading || dataLogHits.isLoading) {
abortController.abort && abortController.abort();
dataLogHits.abortController.abort && dataLogHits.abortController.abort();
} else {
setQuery(tmpQuery); setQuery(tmpQuery);
handleRunQuery(); handleRunQuery();
}; }
}, [isLoading, dataLogHits.isLoading]);
useEffect(() => { useEffect(() => {
if (query) handleRunQuery(); if (query) handleRunQuery();
@ -93,8 +97,8 @@ const ExploreLogs: FC = () => {
onChange={setTmpQuery} onChange={setTmpQuery}
onChangeLimit={handleChangeLimit} onChangeLimit={handleChangeLimit}
onRun={handleUpdateQuery} onRun={handleUpdateQuery}
isLoading={isLoading || dataLogHits.isLoading}
/> />
{isLoading && <Spinner message={"Loading logs..."}/>}
{error && <Alert variant="error">{error}</Alert>} {error && <Alert variant="error">{error}</Alert>}
{!error && ( {!error && (
<ExploreLogsBarChart <ExploreLogsBarChart
@ -102,10 +106,12 @@ const ExploreLogs: FC = () => {
query={query} query={query}
period={period} period={period}
onApplyFilter={handleApplyFilter} onApplyFilter={handleApplyFilter}
isLoading={isLoading ? false : dataLogHits.isLoading}
/> />
)} )}
<ExploreLogsBody data={logs}/> <ExploreLogsBody
data={logs}
isLoading={isLoading}
/>
</div> </div>
); );
}; };

View file

@ -9,7 +9,7 @@ import { AlignedData } from "uplot";
import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart"; import BarHitsChart from "../../../components/Chart/BarHitsChart/BarHitsChart";
import Alert from "../../../components/Main/Alert/Alert"; import Alert from "../../../components/Main/Alert/Alert";
import { TimeParams } from "../../../types"; import { TimeParams } from "../../../types";
import Spinner from "../../../components/Main/Spinner/Spinner"; import LineLoader from "../../../components/Main/LineLoader/LineLoader";
interface Props { interface Props {
query: string; query: string;
@ -72,10 +72,7 @@ const ExploreLogsBarChart: FC<Props> = ({ logHits, period, error, isLoading, onA
"vm-block_mobile": isMobile, "vm-block_mobile": isMobile,
})} })}
> >
{isLoading && <Spinner {isLoading && <LineLoader/>}
message={"Loading hits stats..."}
containerStyles={{ position: "absolute" }}
/>}
{!error && noDataMessage && ( {!error && noDataMessage && (
<div className="vm-explore-logs-chart__empty"> <div className="vm-explore-logs-chart__empty">
<Alert variant="info">{noDataMessage}</Alert> <Alert variant="info">{noDataMessage}</Alert>

View file

@ -16,9 +16,11 @@ import TableLogs from "./TableLogs";
import GroupLogs from "../GroupLogs/GroupLogs"; import GroupLogs from "../GroupLogs/GroupLogs";
import { DATE_TIME_FORMAT } from "../../../constants/date"; import { DATE_TIME_FORMAT } from "../../../constants/date";
import { marked } from "marked"; import { marked } from "marked";
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
export interface ExploreLogBodyProps { export interface ExploreLogBodyProps {
data: Logs[]; data: Logs[];
isLoading: boolean;
} }
enum DisplayType { enum DisplayType {
@ -33,7 +35,7 @@ const tabs = [
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> }, { label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
]; ];
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data }) => { const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
const { isMobile } = useDeviceDetect(); const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState(); const { timezone } = useTimeState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const { setSearchParamsFromKeys } = useSearchParamsFromObject();
@ -75,6 +77,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data }) => {
"vm-block_mobile": isMobile, "vm-block_mobile": isMobile,
})} })}
> >
{isLoading && <LineLoader/>}
<div <div
className={classNames({ className={classNames({
"vm-explore-logs-body-header": true, "vm-explore-logs-body-header": true,

View file

@ -1,6 +1,8 @@
@use "src/styles/variables" as *; @use "src/styles/variables" as *;
.vm-explore-logs-body { .vm-explore-logs-body {
position: relative;
&-header { &-header {
margin: -$padding-medium 0-$padding-medium 0; margin: -$padding-medium 0-$padding-medium 0;

View file

@ -1,5 +1,5 @@
import React, { FC, useEffect, useState } from "preact/compat"; import React, { FC, useEffect, useState } from "preact/compat";
import { InfoIcon, PlayIcon, WikiIcon } from "../../../components/Main/Icons"; import { InfoIcon, PlayIcon, SpinnerIcon, WikiIcon } from "../../../components/Main/Icons";
import "./style.scss"; import "./style.scss";
import classNames from "classnames"; import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect"; import useDeviceDetect from "../../../hooks/useDeviceDetect";
@ -11,6 +11,7 @@ export interface ExploreLogHeaderProps {
query: string; query: string;
limit: number; limit: number;
error?: string; error?: string;
isLoading: boolean;
onChange: (val: string) => void; onChange: (val: string) => void;
onChangeLimit: (val: number) => void; onChangeLimit: (val: number) => void;
onRun: () => void; onRun: () => void;
@ -20,6 +21,7 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
query, query,
limit, limit,
error, error,
isLoading,
onChange, onChange,
onChangeLimit, onChangeLimit,
onRun, onRun,
@ -94,13 +96,16 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
Documentation Documentation
</a> </a>
</div> </div>
<div className="vm-explore-logs-header-bottom__execute"> <div className="vm-explore-logs-header-bottom-execute">
<Button <Button
startIcon={<PlayIcon/>} startIcon={isLoading ? <SpinnerIcon/> : <PlayIcon/>}
onClick={onRun} onClick={onRun}
fullWidth fullWidth
> >
Execute Query <span className="vm-explore-logs-header-bottom-execute__text">
{isLoading ? "Cancel Query" : "Execute Query"}
</span>
<span className="vm-explore-logs-header-bottom-execute__text_hidden">Execute Query</span>
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -29,8 +29,18 @@
flex-grow: 1; flex-grow: 1;
} }
&__execute { &-execute {
position: relative;
display: grid; display: grid;
&__text {
position: absolute;
&_hidden {
position: relative;
visibility: hidden;
}
}
} }
&-helpful { &-helpful {

View file

@ -118,5 +118,6 @@ export const useFetchLogHits = (server: string, query: string) => {
isLoading: Object.values(isLoading).some(s => s), isLoading: Object.values(isLoading).some(s => s),
error, error,
fetchLogHits, fetchLogHits,
abortController: abortControllerRef.current
}; };
}; };

View file

@ -81,5 +81,6 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
isLoading: Object.values(isLoading).some(s => s), isLoading: Object.values(isLoading).some(s => s),
error, error,
fetchLogs, fetchLogs,
abortController: abortControllerRef.current
}; };
}; };

View file

@ -17,6 +17,7 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
* FEATURE: add support for forced merge. See [these docs](https://docs.victoriametrics.com/victorialogs/#forced-merge). * FEATURE: add support for forced merge. See [these docs](https://docs.victoriametrics.com/victorialogs/#forced-merge).
* FEATURE: skip empty log fields in query results, since they are treated as non-existing fields in [VictoriaLogs data model](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model). * FEATURE: skip empty log fields in query results, since they are treated as non-existing fields in [VictoriaLogs data model](https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model).
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add the ability to cancel running queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7097).
* BUGFIX: avoid possible panic when logs for a new day are ingested during execution of concurrent queries. * BUGFIX: avoid possible panic when logs for a new day are ingested during execution of concurrent queries.
* BUGFIX: avoid panic at `lib/logstorage.(*blockResultColumn).forEachDictValue()` when [stats with additional filters](https://docs.victoriametrics.com/victorialogs/logsql/#stats-with-additional-filters). The panic has been introduced in [v0.33.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.33.0-victorialogs) in [this commit](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/a350be48b68330ee1a487e1fb09b002d3be45163). * BUGFIX: avoid panic at `lib/logstorage.(*blockResultColumn).forEachDictValue()` when [stats with additional filters](https://docs.victoriametrics.com/victorialogs/logsql/#stats-with-additional-filters). The panic has been introduced in [v0.33.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.33.0-victorialogs) in [this commit](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/a350be48b68330ee1a487e1fb09b002d3be45163).

View file

@ -24,6 +24,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/) and [Single-node VictoriaMetrics](https://docs.victoriametrics.com/): add support of [exponential histograms](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram) ingested via [OpenTelemetry protocol for metrics](https://docs.victoriametrics.com/#sending-data-via-opentelemetry). Such histograms will be automatically converted to [VictoriaMetrics histogram format](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350). See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6354). * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/) and [Single-node VictoriaMetrics](https://docs.victoriametrics.com/): add support of [exponential histograms](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram) ingested via [OpenTelemetry protocol for metrics](https://docs.victoriametrics.com/#sending-data-via-opentelemetry). Such histograms will be automatically converted to [VictoriaMetrics histogram format](https://valyala.medium.com/improving-histogram-usability-for-prometheus-and-grafana-bc7e5df0e350). See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6354).
* FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/): disable stream processing mode for data [ingested via InfluxDB](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) HTTP endpoints by default. With this change, the data is processed in batches (see `-influx.maxRequestSize`) and user will get parsing errors immediately as they happen. This also improves users' experience and resiliency against thundering herd problems caused by clients without backoff policies like telegraf. To enable stream mode back, pass HTTP header `Stream-Mode: "1"` with each request. For data sent via TCP and UDP (see `-influxListenAddr`) protocols streaming processing remains enabled. * FEATURE: [vmsingle](https://docs.victoriametrics.com/single-server-victoriametrics/), `vminsert` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/) and [vmagent](https://docs.victoriametrics.com/vmagent/): disable stream processing mode for data [ingested via InfluxDB](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) HTTP endpoints by default. With this change, the data is processed in batches (see `-influx.maxRequestSize`) and user will get parsing errors immediately as they happen. This also improves users' experience and resiliency against thundering herd problems caused by clients without backoff policies like telegraf. To enable stream mode back, pass HTTP header `Stream-Mode: "1"` with each request. For data sent via TCP and UDP (see `-influxListenAddr`) protocols streaming processing remains enabled.
* FEATURE: [vmgateway](https://docs.victoriametrics.com/vmgateway/): allow parsing `scope` claim parsing in array format. This is useful for cases when identity provider does encode claims in array format. * FEATURE: [vmgateway](https://docs.victoriametrics.com/vmgateway/): allow parsing `scope` claim parsing in array format. This is useful for cases when identity provider does encode claims in array format.
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to cancel running queries. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7097).
* BUGFIX: [vmgateway](https://docs.victoriametrics.com/vmgateway/): fix possible panic during parsing of a token without `vm_access` claim. This issue was introduced in v1.104.0. * BUGFIX: [vmgateway](https://docs.victoriametrics.com/vmgateway/): fix possible panic during parsing of a token without `vm_access` claim. This issue was introduced in v1.104.0.