mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
app/vmui: update cardinality page (#3986)
vmui: update cardinality page --------- Co-authored-by: Yury Moladau <yurymolodov@gmail.com>
This commit is contained in:
parent
5f77efa915
commit
352dbd7e08
27 changed files with 900 additions and 463 deletions
|
@ -1,6 +1,5 @@
|
||||||
export interface CardinalityRequestsParams {
|
export interface CardinalityRequestsParams {
|
||||||
topN: number,
|
topN: number,
|
||||||
extraLabel: string | null,
|
|
||||||
match: string | null,
|
match: string | null,
|
||||||
date: string | null,
|
date: string | null,
|
||||||
focusLabel: string | null,
|
focusLabel: string | null,
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { FC, useEffect, useState } from "preact/compat";
|
||||||
|
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
type BarChartData = {
|
||||||
|
value: number,
|
||||||
|
name: string,
|
||||||
|
percentage?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimpleBarChartProps {
|
||||||
|
data: BarChartData[],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SimpleBarChart: FC<SimpleBarChartProps> = ({ data }) => {
|
||||||
|
|
||||||
|
const [bars, setBars] = useState<BarChartData[]>([]);
|
||||||
|
const [yAxis, setYAxis] = useState([0, 0]);
|
||||||
|
|
||||||
|
const generateYAxis = (sortedValues: BarChartData[]) => {
|
||||||
|
const numbers = sortedValues.map(b => b.value);
|
||||||
|
const max = Math.ceil(numbers[0] || 1);
|
||||||
|
const ticks = 10;
|
||||||
|
const step = max / (ticks - 1);
|
||||||
|
return new Array(ticks + 1).fill(max + step).map((v, i) => Math.round(v - (step * i)));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sortedValues = data.sort((a, b) => b.value - a.value);
|
||||||
|
const yAxis = generateYAxis(sortedValues);
|
||||||
|
setYAxis(yAxis);
|
||||||
|
|
||||||
|
setBars(sortedValues.map(b => ({
|
||||||
|
...b,
|
||||||
|
percentage: (b.value / yAxis[0]) * 100,
|
||||||
|
})));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vm-simple-bar-chart">
|
||||||
|
<div className="vm-simple-bar-chart-y-axis">
|
||||||
|
{yAxis.map(v => (
|
||||||
|
<div
|
||||||
|
className="vm-simple-bar-chart-y-axis__tick"
|
||||||
|
key={v}
|
||||||
|
>{v}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="vm-simple-bar-chart-data">
|
||||||
|
{bars.map(({ name, value, percentage }) => (
|
||||||
|
<Tooltip
|
||||||
|
title={`${name}: ${value}`}
|
||||||
|
key={`${name}_${value}`}
|
||||||
|
placement="top-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="vm-simple-bar-chart-data-item"
|
||||||
|
style={{ maxHeight: `${percentage || 0}%` }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SimpleBarChart;
|
|
@ -0,0 +1,74 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
$color-bar: #33BB55;
|
||||||
|
$color-bar-highest: #F79420;
|
||||||
|
|
||||||
|
.vm-simple-bar-chart {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: #{$font-size-small/2};
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&-y-axis {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
transform: translateY(#{$font-size-small});
|
||||||
|
|
||||||
|
&__tick {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: $padding-small;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
line-height: 2;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
left: 100%;
|
||||||
|
width: 100vw;
|
||||||
|
height: 0;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
transform: translateY(-1px) translateZ(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-data {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1%;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1px;
|
||||||
|
height: calc(100% - ($font-size-small*4));
|
||||||
|
background-color: $color-bar;
|
||||||
|
transition: background-color 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($color-bar, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
background-color: $color-bar-highest;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($color-bar-highest, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { FC, useMemo, useRef } from "preact/compat";
|
import React, { FC, useEffect, useMemo, useRef } from "preact/compat";
|
||||||
import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Button from "../../Main/Button/Button";
|
import Button from "../../Main/Button/Button";
|
||||||
import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons";
|
import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons";
|
||||||
|
@ -8,21 +7,28 @@ import { getAppModeEnable } from "../../../utils/app-mode";
|
||||||
import { DATE_FORMAT } from "../../../constants/date";
|
import { DATE_FORMAT } from "../../../constants/date";
|
||||||
import DatePicker from "../../Main/DatePicker/DatePicker";
|
import DatePicker from "../../Main/DatePicker/DatePicker";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const CardinalityDatePicker: FC = () => {
|
const CardinalityDatePicker: FC = () => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
const buttonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { date } = useCardinalityState();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const cardinalityDispatch = useCardinalityDispatch();
|
|
||||||
|
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
|
||||||
|
|
||||||
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
|
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
|
||||||
|
|
||||||
const handleChangeDate = (val: string) => {
|
const handleChangeDate = (val: string) => {
|
||||||
cardinalityDispatch({ type: "SET_DATE", payload: val });
|
searchParams.set("date", val);
|
||||||
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleChangeDate(date);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div ref={buttonRef}>
|
<div ref={buttonRef}>
|
||||||
|
|
|
@ -387,3 +387,14 @@ export const TuneIcon = () => (
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const TipIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 20h4c0 1.1-.9 2-2 2s-2-.9-2-2zm-2-1h8v-2H5v2zm11.5-9.5c0 3.82-2.66 5.86-3.77 6.5H5.27c-1.11-.64-3.77-2.68-3.77-6.5C1.5 5.36 4.86 2 9 2s7.5 3.36 7.5 7.5zm4.87-2.13L20 8l1.37.63L22 10l.63-1.37L24 8l-1.37-.63L22 6l-.63 1.37zM19 6l.94-2.06L22 3l-2.06-.94L19 0l-.94 2.06L16 3l2.06.94L19 6z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { TimeStateProvider } from "../state/time/TimeStateContext";
|
||||||
import { QueryStateProvider } from "../state/query/QueryStateContext";
|
import { QueryStateProvider } from "../state/query/QueryStateContext";
|
||||||
import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
|
import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
|
||||||
import { GraphStateProvider } from "../state/graph/GraphStateContext";
|
import { GraphStateProvider } from "../state/graph/GraphStateContext";
|
||||||
import { CardinalityStateProvider } from "../state/cardinality/CardinalityStateContext";
|
|
||||||
import { TopQueriesStateProvider } from "../state/topQueries/TopQueriesStateContext";
|
import { TopQueriesStateProvider } from "../state/topQueries/TopQueriesStateContext";
|
||||||
import { SnackbarProvider } from "./Snackbar";
|
import { SnackbarProvider } from "./Snackbar";
|
||||||
|
|
||||||
|
@ -16,7 +15,6 @@ const providers = [
|
||||||
QueryStateProvider,
|
QueryStateProvider,
|
||||||
CustomPanelStateProvider,
|
CustomPanelStateProvider,
|
||||||
GraphStateProvider,
|
GraphStateProvider,
|
||||||
CardinalityStateProvider,
|
|
||||||
TopQueriesStateProvider,
|
TopQueriesStateProvider,
|
||||||
SnackbarProvider,
|
SnackbarProvider,
|
||||||
DashboardsStateProvider
|
DashboardsStateProvider
|
||||||
|
|
|
@ -1,96 +1,76 @@
|
||||||
import React, { FC, useMemo } from "react";
|
import React, { FC, useMemo } from "react";
|
||||||
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
import { PlayIcon, QuestionIcon, RestartIcon, TipIcon, WikiIcon } from "../../../components/Main/Icons";
|
||||||
import { useFetchQueryOptions } from "../../../hooks/useFetchQueryOptions";
|
|
||||||
import { ErrorTypes } from "../../../types";
|
|
||||||
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
|
|
||||||
import Switch from "../../../components/Main/Switch/Switch";
|
|
||||||
import { InfoIcon, PlayIcon, QuestionIcon, WikiIcon } from "../../../components/Main/Icons";
|
|
||||||
import Button from "../../../components/Main/Button/Button";
|
import Button from "../../../components/Main/Button/Button";
|
||||||
import TextField from "../../../components/Main/TextField/TextField";
|
import TextField from "../../../components/Main/TextField/TextField";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useEffect, useState } from "preact/compat";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import CardinalityTotals, { CardinalityTotalsProps } from "../CardinalityTotals/CardinalityTotals";
|
||||||
|
|
||||||
export interface CardinalityConfiguratorProps {
|
const CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
|
||||||
onSetHistory: (step: number) => void;
|
|
||||||
onSetQuery: (query: string) => void;
|
|
||||||
onRunQuery: () => void;
|
|
||||||
onTopNChange: (value: string) => void;
|
|
||||||
onFocusLabelChange: (value: string) => void;
|
|
||||||
query: string;
|
|
||||||
topN: number;
|
|
||||||
error?: ErrorTypes | string;
|
|
||||||
totalSeries: number;
|
|
||||||
totalLabelValuePairs: number;
|
|
||||||
date: string | null;
|
|
||||||
match: string | null;
|
|
||||||
focusLabel: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|
||||||
topN,
|
|
||||||
error,
|
|
||||||
query,
|
|
||||||
onSetHistory,
|
|
||||||
onRunQuery,
|
|
||||||
onSetQuery,
|
|
||||||
onTopNChange,
|
|
||||||
onFocusLabelChange,
|
|
||||||
totalSeries,
|
|
||||||
totalLabelValuePairs,
|
|
||||||
date,
|
|
||||||
match,
|
|
||||||
focusLabel
|
|
||||||
}) => {
|
|
||||||
const { autocomplete } = useQueryState();
|
|
||||||
const queryDispatch = useQueryDispatch();
|
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const { queryOptions } = useFetchQueryOptions();
|
const showTips = searchParams.get("tips") || "";
|
||||||
|
const [match, setMatch] = useState(searchParams.get("match") || "");
|
||||||
|
const [focusLabel, setFocusLabel] = useState(searchParams.get("focusLabel") || "");
|
||||||
|
const [topN, setTopN] = useState(+(searchParams.get("topN") || 10));
|
||||||
|
|
||||||
const errorTopN = useMemo(() => topN < 1 ? "Number must be bigger than zero" : "", [topN]);
|
const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]);
|
||||||
|
|
||||||
const onChangeAutocomplete = () => {
|
const handleTopNChange = (val: string) => {
|
||||||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
const num = +val;
|
||||||
|
setTopN(isNaN(num) ? 0 : num);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArrowUp = () => {
|
const handleRunQuery = () => {
|
||||||
onSetHistory(-1);
|
searchParams.set("match", match);
|
||||||
|
searchParams.set("topN", topN.toString());
|
||||||
|
searchParams.set("focusLabel", focusLabel);
|
||||||
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArrowDown = () => {
|
const handleResetQuery = () => {
|
||||||
onSetHistory(1);
|
searchParams.set("match", "");
|
||||||
|
searchParams.set("focusLabel", "");
|
||||||
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleTips = () => {
|
||||||
|
const showTips = searchParams.get("tips") || "";
|
||||||
|
if (showTips) searchParams.delete("tips");
|
||||||
|
else searchParams.set("tips", "true");
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const matchQuery = searchParams.get("match");
|
||||||
|
const topNQuery = +(searchParams.get("topN") || 10);
|
||||||
|
const focusLabelQuery = searchParams.get("focusLabel");
|
||||||
|
if (matchQuery !== match) setMatch(matchQuery || "");
|
||||||
|
if (topNQuery !== topN) setTopN(topNQuery);
|
||||||
|
if (focusLabelQuery !== focusLabel) setFocusLabel(focusLabelQuery || "");
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-cardinality-configurator": true,
|
"vm-cardinality-configurator": true,
|
||||||
|
"vm-cardinality-configurator_mobile": isMobile,
|
||||||
"vm-block": true,
|
"vm-block": true,
|
||||||
"vm-block_mobile": isMobile,
|
"vm-block_mobile": isMobile,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="vm-cardinality-configurator-controls">
|
<div className="vm-cardinality-configurator-controls">
|
||||||
<div className="vm-cardinality-configurator-controls__query">
|
<div className="vm-cardinality-configurator-controls__query">
|
||||||
<QueryEditor
|
|
||||||
value={query}
|
|
||||||
autocomplete={autocomplete}
|
|
||||||
options={queryOptions}
|
|
||||||
error={error}
|
|
||||||
onArrowUp={handleArrowUp}
|
|
||||||
onArrowDown={handleArrowDown}
|
|
||||||
onEnter={onRunQuery}
|
|
||||||
onChange={onSetQuery}
|
|
||||||
label={"Time series selector"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="vm-cardinality-configurator-controls__item">
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Number of entries per table"
|
label="Time series selector"
|
||||||
type="number"
|
type="string"
|
||||||
value={topN}
|
value={match}
|
||||||
error={errorTopN}
|
onChange={setMatch}
|
||||||
onChange={onTopNChange}
|
onEnter={handleRunQuery}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-cardinality-configurator-controls__item">
|
<div className="vm-cardinality-configurator-controls__item">
|
||||||
|
@ -98,41 +78,36 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
label="Focus label"
|
label="Focus label"
|
||||||
type="text"
|
type="text"
|
||||||
value={focusLabel || ""}
|
value={focusLabel || ""}
|
||||||
onChange={onFocusLabelChange}
|
onChange={setFocusLabel}
|
||||||
|
onEnter={handleRunQuery}
|
||||||
endIcon={(
|
endIcon={(
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={(
|
title={(
|
||||||
<div>
|
<div>
|
||||||
<p>To identify values with the highest number of series for the selected label.</p>
|
<p>To identify values with the highest number of series for the selected label.</p>
|
||||||
<p>Adds a table showing the series with the highest number of series.</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<InfoIcon/>
|
<QuestionIcon/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="vm-cardinality-configurator-controls__item vm-cardinality-configurator-controls__item_limit">
|
||||||
<div className="vm-cardinality-configurator-additional">
|
<TextField
|
||||||
<Switch
|
label="Limit entries"
|
||||||
label={"Autocomplete"}
|
type="number"
|
||||||
value={autocomplete}
|
value={topN}
|
||||||
onChange={onChangeAutocomplete}
|
error={errorTopN}
|
||||||
/>
|
onChange={handleTopNChange}
|
||||||
</div>
|
onEnter={handleRunQuery}
|
||||||
<div
|
/>
|
||||||
className={classNames({
|
|
||||||
"vm-cardinality-configurator-bottom": true,
|
|
||||||
"vm-cardinality-configurator-bottom_mobile": isMobile,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="vm-cardinality-configurator-bottom__info">
|
|
||||||
Analyzed <b>{totalSeries}</b> series with <b>{totalLabelValuePairs}</b> "label=value" pairs
|
|
||||||
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
|
|
||||||
Show top {topN} entries per table.
|
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-cardinality-configurator-bottom__docs">
|
</div>
|
||||||
|
<div className="vm-cardinality-configurator-bottom">
|
||||||
|
<CardinalityTotals {...props}/>
|
||||||
|
|
||||||
|
<div className="vm-cardinality-configurator-bottom-helpful">
|
||||||
<a
|
<a
|
||||||
className="vm-link vm-link_with-icon"
|
className="vm-link vm-link_with-icon"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -140,25 +115,33 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
rel="help noreferrer"
|
rel="help noreferrer"
|
||||||
>
|
>
|
||||||
<WikiIcon/>
|
<WikiIcon/>
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="vm-link vm-link_with-icon"
|
|
||||||
target="_blank"
|
|
||||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
|
||||||
rel="help noreferrer"
|
|
||||||
>
|
|
||||||
<QuestionIcon/>
|
|
||||||
Example of using
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
startIcon={<PlayIcon/>}
|
<div className="vm-cardinality-configurator-bottom__execute">
|
||||||
onClick={onRunQuery}
|
<Tooltip title={showTips ? "Hide tips" : "Show tips"}>
|
||||||
fullWidth
|
<Button
|
||||||
>
|
variant="text"
|
||||||
Execute Query
|
color={showTips ? "warning" : "gray"}
|
||||||
</Button>
|
startIcon={<TipIcon/>}
|
||||||
|
onClick={handleToggleTips}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
startIcon={<RestartIcon/>}
|
||||||
|
onClick={handleResetQuery}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<PlayIcon/>}
|
||||||
|
onClick={handleRunQuery}
|
||||||
|
>
|
||||||
|
Execute Query
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,58 +9,65 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 $padding-medium;
|
gap: $padding-small $padding-medium;
|
||||||
|
|
||||||
&__query {
|
&__query {
|
||||||
flex-grow: 8;
|
flex-grow: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
flex-grow: 1;
|
flex-grow: 2;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-additional {
|
&_limit {
|
||||||
display: flex;
|
flex-grow: 1;
|
||||||
align-items: center;
|
}
|
||||||
margin-bottom: $padding-small;
|
|
||||||
|
svg {
|
||||||
|
color: $color-text-disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bottom {
|
&-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&__docs {
|
&-helpful {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $padding-global;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $padding-small $padding-global;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color-text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_mobile &__docs {
|
&__execute {
|
||||||
justify-content: space-between;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $padding-small;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__info {
|
&_mobile &-bottom {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&-helpful {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: $font-size;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
&__execute {
|
||||||
color: $color-text-secondary;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button:nth-child(3) {
|
||||||
margin: 0 0 0 auto;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
&_mobile {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { TipIcon } from "../../../components/Main/Icons";
|
||||||
|
import React, { FC } from "preact/compat";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
const Link: FC<{ href: string, children: ReactNode, target?: string }> = ({ href, children, target }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="vm-link vm-link_colored"
|
||||||
|
target={target}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TipCard: FC<{ title?: string, children: ReactNode }> = ({ title, children }) => (
|
||||||
|
<div className="vm-cardinality-tip">
|
||||||
|
<div className="vm-cardinality-tip-header">
|
||||||
|
<div className="vm-cardinality-tip-header__tip-icon"><TipIcon/></div>
|
||||||
|
<h4 className="vm-cardinality-tip-header__title">{title || "Tips"}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="vm-cardinality-tip__description">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TipDocumentation: FC = () => (
|
||||||
|
<TipCard title="Cardinality explorer">
|
||||||
|
<h6>Helpful for analyzing VictoriaMetrics TSDB data</h6>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link href="https://docs.victoriametrics.com/#cardinality-explorer">
|
||||||
|
Cardinality explorer documentation
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
See the <Link href="https://victoriametrics.com/blog/cardinality-explorer/">
|
||||||
|
example of using</Link> the cardinality explorer
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</TipCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TipHighNumberOfSeries: FC = () => (
|
||||||
|
<TipCard title="Metrics with a high number of series">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Identify and eliminate labels with frequently changed values to reduce their
|
||||||
|
<Link
|
||||||
|
href='https://docs.victoriametrics.com/FAQ.html#what-is-high-cardinality'
|
||||||
|
target={"_blank"}
|
||||||
|
>cardinality</Link> and
|
||||||
|
<Link
|
||||||
|
href='https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate'
|
||||||
|
target={"_blank"}
|
||||||
|
>high churn rate</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Find unused time series and
|
||||||
|
<Link
|
||||||
|
href='https://docs.victoriametrics.com/relabeling.html'
|
||||||
|
target={"_blank"}
|
||||||
|
>drop entire metrics</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Aggregate time series before they got ingested into the database via
|
||||||
|
<Link
|
||||||
|
href='https://docs.victoriametrics.com/stream-aggregation.html'
|
||||||
|
target={"_blank"}
|
||||||
|
>streaming aggregation</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</TipCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TipHighNumberOfValues: FC = () => (
|
||||||
|
<TipCard title="Labels with a high number of unique values">
|
||||||
|
<ul>
|
||||||
|
<li>Decrease the number of unique label values to reduce cardinality</li>
|
||||||
|
<li>Drop the label entirely via
|
||||||
|
<Link
|
||||||
|
href='https://docs.victoriametrics.com/relabeling.html'
|
||||||
|
target={"_blank"}
|
||||||
|
>relabeling</Link></li>
|
||||||
|
<li>For volatile label values (such as URL path, user session, etc.)
|
||||||
|
consider printing them to the log file instead of adding to time series</li>
|
||||||
|
</ul>
|
||||||
|
</TipCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TipCardinalityOfSingle: FC = () => (
|
||||||
|
<TipCard title="Dashboard of a single metric">
|
||||||
|
<p>This dashboard helps to understand the cardinality of a single metric.</p>
|
||||||
|
<p>
|
||||||
|
Each time series is a unique combination of key-value label pairs.
|
||||||
|
Therefore a label key with many values can create a lot of time series for a particular metric.
|
||||||
|
If you’re trying to decrease the cardinality of a metric,
|
||||||
|
start by looking at the labels with the highest number of values.
|
||||||
|
</p>
|
||||||
|
<p>Use the series selector at the top of the page to apply additional filters.</p>
|
||||||
|
</TipCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TipCardinalityOfLabel: FC = () => (
|
||||||
|
<TipCard title="Dashboard of a label">
|
||||||
|
<p>
|
||||||
|
This dashboard helps you understand the count of time series per label.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use the selector at the top of the page to pick a label name you’d like to inspect.
|
||||||
|
For the selected label name, you’ll see the label values that have the highest number of series associated with
|
||||||
|
them.
|
||||||
|
So if you’ve chosen `instance` as your label name, you may see that `657` time series have value
|
||||||
|
“host-1” attached to them and `580` time series have value `host-2` attached to them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This can be helpful in allowing you to determine where the bulk of your time series are coming from.
|
||||||
|
If the label “instance=host-1” was applied to 657 series and the label “instance=host-2”
|
||||||
|
was only applied to 580 series, you’d know, for example, that host-01 was responsible for sending
|
||||||
|
the majority of the time series.
|
||||||
|
</p>
|
||||||
|
</TipCard>
|
||||||
|
);
|
|
@ -0,0 +1,87 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-cardinality-tip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
background-color: $color-background-block;
|
||||||
|
border-radius: $border-radius-medium;
|
||||||
|
box-shadow: $box-shadow;
|
||||||
|
overflow: hidden;
|
||||||
|
color: $color-text-secondary;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $padding-small $padding-global;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: $color-warning;
|
||||||
|
opacity: 0.1;
|
||||||
|
pointer-events: none
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tip-icon {
|
||||||
|
width: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $color-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip {
|
||||||
|
max-width: 280px;
|
||||||
|
white-space: normal;
|
||||||
|
padding: $padding-small;
|
||||||
|
line-height: 130%;
|
||||||
|
font-size: $font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
padding: $padding-small $padding-global;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: $padding-small;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: $font-size-medium;
|
||||||
|
margin-bottom: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
margin-bottom: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
list-style-position: inside;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: calc($padding-small/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { FC } from "preact/compat";
|
||||||
|
import { InfoIcon } from "../../../components/Main/Icons";
|
||||||
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
|
import { TopHeapEntry } from "../types";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
export interface CardinalityTotalsProps {
|
||||||
|
totalSeries: number;
|
||||||
|
totalSeriesAll: number;
|
||||||
|
totalLabelValuePairs: number;
|
||||||
|
seriesCountByMetricName: TopHeapEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardinalityTotals: FC<CardinalityTotalsProps> = ({
|
||||||
|
totalSeries,
|
||||||
|
totalSeriesAll,
|
||||||
|
seriesCountByMetricName
|
||||||
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const match = searchParams.get("match");
|
||||||
|
const focusLabel = searchParams.get("focusLabel");
|
||||||
|
const isMetric = /__name__/.test(match || "");
|
||||||
|
|
||||||
|
const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100;
|
||||||
|
|
||||||
|
const totals = [
|
||||||
|
{
|
||||||
|
title: "Total series",
|
||||||
|
value: totalSeries.toLocaleString("en-US"),
|
||||||
|
display: !focusLabel,
|
||||||
|
info: `The total number of active time series.
|
||||||
|
A time series is uniquely identified by its name plus a set of its labels.
|
||||||
|
For example, temperature{city="NY",country="US"} and temperature{city="SF",country="US"}
|
||||||
|
are two distinct series, since they differ by the city label.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Percentage from total",
|
||||||
|
value: isNaN(progress) ? "-" : `${progress.toFixed(2)}%`,
|
||||||
|
display: isMetric,
|
||||||
|
info: "The share of these series in the total number of time series."
|
||||||
|
}
|
||||||
|
].filter(t => t.display);
|
||||||
|
|
||||||
|
if (!totals.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-cardinality-totals": true,
|
||||||
|
"vm-cardinality-totals_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{totals.map(({ title, value, info }) => (
|
||||||
|
<div
|
||||||
|
className="vm-cardinality-totals-card"
|
||||||
|
key={title}
|
||||||
|
>
|
||||||
|
<div className="vm-cardinality-totals-card-header">
|
||||||
|
{info && (
|
||||||
|
<Tooltip title={<p className="vm-cardinality-totals-card-header__tooltip">{info}</p>}>
|
||||||
|
<div className="vm-cardinality-totals-card-header__info-icon"><InfoIcon/></div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<h4 className="vm-cardinality-totals-card-header__title">{title}</h4>
|
||||||
|
</div>
|
||||||
|
<span className="vm-cardinality-totals-card__value">{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardinalityTotals;
|
|
@ -0,0 +1,60 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-cardinality-totals {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: $padding-global;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
gap: $padding-small;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&__info-icon {
|
||||||
|
width: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-text;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: ':';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip {
|
||||||
|
max-width: 280px;
|
||||||
|
white-space: normal;
|
||||||
|
padding: $padding-small;
|
||||||
|
line-height: 130%;
|
||||||
|
font-size: $font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-primary;
|
||||||
|
font-size: $font-size-medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,43 +1,40 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import EnhancedTable from "../Table/Table";
|
import EnhancedTable from "../Table/Table";
|
||||||
import TableCells from "../Table/TableCells/TableCells";
|
import TableCells from "../Table/TableCells/TableCells";
|
||||||
import BarChart from "../../../components/Chart/BarChart/BarChart";
|
|
||||||
import { barOptions } from "../../../components/Chart/BarChart/consts";
|
|
||||||
import { Data, HeadCell } from "../Table/types";
|
import { Data, HeadCell } from "../Table/types";
|
||||||
import { MutableRef } from "preact/hooks";
|
import { MutableRef } from "preact/hooks";
|
||||||
import Tabs from "../../../components/Main/Tabs/Tabs";
|
import Tabs from "../../../components/Main/Tabs/Tabs";
|
||||||
import { useMemo } from "preact/compat";
|
import { useMemo, useState } from "preact/compat";
|
||||||
import { ChartIcon, TableIcon } from "../../../components/Main/Icons";
|
import { ChartIcon, InfoIcon, TableIcon } 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";
|
||||||
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
|
import SimpleBarChart from "../../../components/Chart/SimpleBarChart/SimpleBarChart";
|
||||||
|
|
||||||
interface MetricsProperties {
|
interface MetricsProperties {
|
||||||
rows: Data[];
|
rows: Data[];
|
||||||
activeTab: number;
|
|
||||||
onChange: (newValue: string, tabId: string) => void;
|
|
||||||
onActionClick: (name: string) => void;
|
onActionClick: (name: string) => void;
|
||||||
tabs: string[];
|
tabs: string[];
|
||||||
chartContainer: MutableRef<HTMLDivElement> | undefined;
|
chartContainer: MutableRef<HTMLDivElement> | undefined;
|
||||||
totalSeries: number,
|
totalSeries: number,
|
||||||
tabId: string;
|
|
||||||
sectionTitle: string;
|
sectionTitle: string;
|
||||||
|
tip?: string;
|
||||||
tableHeaderCells: HeadCell[];
|
tableHeaderCells: HeadCell[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MetricsContent: FC<MetricsProperties> = ({
|
const MetricsContent: FC<MetricsProperties> = ({
|
||||||
rows,
|
rows,
|
||||||
activeTab,
|
tabs: tabsProps = [],
|
||||||
onChange,
|
|
||||||
tabs: tabsProps,
|
|
||||||
chartContainer,
|
chartContainer,
|
||||||
totalSeries,
|
totalSeries,
|
||||||
tabId,
|
|
||||||
onActionClick,
|
onActionClick,
|
||||||
sectionTitle,
|
sectionTitle,
|
||||||
|
tip,
|
||||||
tableHeaderCells,
|
tableHeaderCells,
|
||||||
}) => {
|
}) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
const [activeTab, setActiveTab] = useState("table");
|
||||||
|
|
||||||
const tableCells = (row: Data) => (
|
const tableCells = (row: Data) => (
|
||||||
<TableCells
|
<TableCells
|
||||||
|
@ -48,15 +45,11 @@ const MetricsContent: FC<MetricsProperties> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const tabs = useMemo(() => tabsProps.map((t, i) => ({
|
const tabs = useMemo(() => tabsProps.map((t, i) => ({
|
||||||
value: String(i),
|
value: t,
|
||||||
label: t,
|
label: t,
|
||||||
icon: i === 0 ? <TableIcon /> : <ChartIcon />
|
icon: i === 0 ? <TableIcon /> : <ChartIcon />
|
||||||
})), [tabsProps]);
|
})), [tabsProps]);
|
||||||
|
|
||||||
const handleChangeTab = (newValue: string) => {
|
|
||||||
onChange(newValue, tabId);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -69,47 +62,53 @@ const MetricsContent: FC<MetricsProperties> = ({
|
||||||
<div className="vm-metrics-content-header vm-section-header">
|
<div className="vm-metrics-content-header vm-section-header">
|
||||||
<h5
|
<h5
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
"vm-metrics-content-header__title": true,
|
||||||
"vm-section-header__title": true,
|
"vm-section-header__title": true,
|
||||||
"vm-section-header__title_mobile": isMobile,
|
"vm-section-header__title_mobile": isMobile,
|
||||||
})}
|
})}
|
||||||
>{sectionTitle}</h5>
|
>
|
||||||
|
{!isMobile && tip && (
|
||||||
|
<Tooltip
|
||||||
|
title={<p
|
||||||
|
dangerouslySetInnerHTML={{ __html: tip }}
|
||||||
|
className="vm-metrics-content-header__tip"
|
||||||
|
/>}
|
||||||
|
>
|
||||||
|
<div className="vm-metrics-content-header__tip-icon"><InfoIcon/></div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{sectionTitle}
|
||||||
|
</h5>
|
||||||
<div className="vm-section-header__tabs">
|
<div className="vm-section-header__tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
activeItem={String(activeTab)}
|
activeItem={activeTab}
|
||||||
items={tabs}
|
items={tabs}
|
||||||
onChange={handleChangeTab}
|
onChange={setActiveTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
ref={chartContainer}
|
{activeTab === "table" && (
|
||||||
className={classNames({
|
<div
|
||||||
"vm-metrics-content__table": true,
|
ref={chartContainer}
|
||||||
"vm-metrics-content__table_mobile": isMobile
|
className={classNames({
|
||||||
})}
|
"vm-metrics-content__table": true,
|
||||||
>
|
"vm-metrics-content__table_mobile": isMobile
|
||||||
{activeTab === 0 && (
|
})}
|
||||||
|
>
|
||||||
<EnhancedTable
|
<EnhancedTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
headerCells={tableHeaderCells}
|
headerCells={tableHeaderCells}
|
||||||
defaultSortColumn={"value"}
|
defaultSortColumn={"value"}
|
||||||
tableCells={tableCells}
|
tableCells={tableCells}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{activeTab === 1 && (
|
)}
|
||||||
<BarChart
|
{activeTab === "graph" && (
|
||||||
data={[
|
<div className="vm-metrics-content__chart">
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
<SimpleBarChart data={rows.map(({ name, value }) => ({ name, value }))}/>
|
||||||
// @ts-ignore
|
</div>
|
||||||
rows.map((v) => v.name),
|
)}
|
||||||
rows.map((v) => v.value),
|
|
||||||
rows.map((_, i) => i % 12 == 0 ? 1 : i % 10 == 0 ? 2 : 0),
|
|
||||||
]}
|
|
||||||
container={chartContainer?.current || null}
|
|
||||||
configs={barOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,33 @@
|
||||||
.vm-metrics-content {
|
.vm-metrics-content {
|
||||||
&-header {
|
&-header {
|
||||||
margin: -$padding-medium 0-$padding-medium 0;
|
margin: -$padding-medium 0-$padding-medium 0;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tip {
|
||||||
|
max-width: 300px;
|
||||||
|
white-space: normal;
|
||||||
|
padding: $padding-small;
|
||||||
|
line-height: 130%;
|
||||||
|
font-size: $font-size;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: $padding-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tip-icon {
|
||||||
|
width: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $color-primary;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_mobile &-header {
|
&_mobile &-header {
|
||||||
|
@ -30,4 +57,8 @@
|
||||||
&_mobile &__table {
|
&_mobile &__table {
|
||||||
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
|
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__chart {
|
||||||
|
padding-top: $padding-medium;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { FC, useState } from "preact/compat";
|
import React, { FC, useState } from "preact/compat";
|
||||||
import { ChangeEvent, MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
import { Data, Order, TableProps, } from "./types";
|
import { Data, Order, TableProps, } from "./types";
|
||||||
import { EnhancedTableHead } from "./TableHead";
|
import { EnhancedTableHead } from "./TableHead";
|
||||||
import { getComparator, stableSort } from "./helpers";
|
import { getComparator, stableSort } from "./helpers";
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
const EnhancedTable: FC<TableProps> = ({
|
const EnhancedTable: FC<TableProps> = ({
|
||||||
rows,
|
rows,
|
||||||
|
@ -14,7 +13,6 @@ const EnhancedTable: FC<TableProps> = ({
|
||||||
|
|
||||||
const [order, setOrder] = useState<Order>("desc");
|
const [order, setOrder] = useState<Order>("desc");
|
||||||
const [orderBy, setOrderBy] = useState<keyof Data>(defaultSortColumn);
|
const [orderBy, setOrderBy] = useState<keyof Data>(defaultSortColumn);
|
||||||
const [selected, setSelected] = useState<readonly string[]>([]);
|
|
||||||
|
|
||||||
const handleRequestSort = (
|
const handleRequestSort = (
|
||||||
event: MouseEvent<unknown>,
|
event: MouseEvent<unknown>,
|
||||||
|
@ -25,45 +23,13 @@ const EnhancedTable: FC<TableProps> = ({
|
||||||
setOrderBy(property);
|
setOrderBy(property);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAllClick = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (event.target.checked) {
|
|
||||||
const newSelecteds = rows.map((n) => n.name) as string[];
|
|
||||||
setSelected(newSelecteds);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelected([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (name: string) => () => {
|
|
||||||
const selectedIndex = selected.indexOf(name);
|
|
||||||
let newSelected: readonly string[] = [];
|
|
||||||
|
|
||||||
if (selectedIndex === -1) {
|
|
||||||
newSelected = newSelected.concat(selected, name);
|
|
||||||
} else if (selectedIndex === 0) {
|
|
||||||
newSelected = newSelected.concat(selected.slice(1));
|
|
||||||
} else if (selectedIndex === selected.length - 1) {
|
|
||||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
|
||||||
} else if (selectedIndex > 0) {
|
|
||||||
newSelected = newSelected.concat(
|
|
||||||
selected.slice(0, selectedIndex),
|
|
||||||
selected.slice(selectedIndex + 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelected(newSelected);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = (name: string) => selected.indexOf(name) !== -1;
|
|
||||||
const sortedData = stableSort(rows, getComparator(order, orderBy));
|
const sortedData = stableSort(rows, getComparator(order, orderBy));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="vm-table">
|
<table className="vm-table">
|
||||||
<EnhancedTableHead
|
<EnhancedTableHead
|
||||||
numSelected={selected.length}
|
|
||||||
order={order}
|
order={order}
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
onSelectAllClick={handleSelectAllClick}
|
|
||||||
onRequestSort={handleRequestSort}
|
onRequestSort={handleRequestSort}
|
||||||
rowCount={rows.length}
|
rowCount={rows.length}
|
||||||
headerCells={headerCells}
|
headerCells={headerCells}
|
||||||
|
@ -71,12 +37,8 @@ const EnhancedTable: FC<TableProps> = ({
|
||||||
<tbody className="vm-table-header">
|
<tbody className="vm-table-header">
|
||||||
{sortedData.map((row) => (
|
{sortedData.map((row) => (
|
||||||
<tr
|
<tr
|
||||||
className={classNames({
|
className="vm-table__row"
|
||||||
"vm-table__row": true,
|
|
||||||
"vm-table__row_selected": isSelected(row.name)
|
|
||||||
})}
|
|
||||||
key={row.name}
|
key={row.name}
|
||||||
onClick={handleClick(row.name)}
|
|
||||||
>
|
>
|
||||||
{tableCells(row)}
|
{tableCells(row)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -23,7 +23,12 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
|
||||||
className="vm-table-cell"
|
className="vm-table-cell"
|
||||||
key={row.name}
|
key={row.name}
|
||||||
>
|
>
|
||||||
{row.name}
|
<span
|
||||||
|
className="vm-link vm-link_colored"
|
||||||
|
onClick={handleActionClick}
|
||||||
|
>
|
||||||
|
{row.name}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="vm-table-cell"
|
className="vm-table-cell"
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { MouseEvent } from "react";
|
||||||
import React from "preact/compat";
|
import React from "preact/compat";
|
||||||
import { Data, EnhancedHeaderTableProps } from "./types";
|
import { Data, EnhancedHeaderTableProps } from "./types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ArrowDropDownIcon } from "../../../components/Main/Icons";
|
import { ArrowDropDownIcon, InfoIcon } from "../../../components/Main/Icons";
|
||||||
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
|
|
||||||
export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
|
export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
|
||||||
const { order, orderBy, onRequestSort, headerCells } = props;
|
const { order, orderBy, onRequestSort, headerCells } = props;
|
||||||
|
@ -24,7 +25,13 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
|
||||||
onClick={createSortHandler(headCell.id as keyof Data)}
|
onClick={createSortHandler(headCell.id as keyof Data)}
|
||||||
>
|
>
|
||||||
<div className="vm-table-cell__content">
|
<div className="vm-table-cell__content">
|
||||||
{headCell.label}
|
{
|
||||||
|
headCell.info ?
|
||||||
|
<Tooltip title={headCell.info}>
|
||||||
|
<div className="vm-metrics-content-header__tip-icon"><InfoIcon /></div>
|
||||||
|
{headCell.label}
|
||||||
|
</Tooltip>: <>{headCell.label}</>
|
||||||
|
}
|
||||||
{headCell.id !== "action" && headCell.id !== "percentage" && (
|
{headCell.id !== "action" && headCell.id !== "percentage" && (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { ChangeEvent, MouseEvent, ReactNode } from "react";
|
import { MouseEvent, ReactNode } from "react";
|
||||||
|
|
||||||
export type Order = "asc" | "desc";
|
export type Order = "asc" | "desc";
|
||||||
|
|
||||||
export interface HeadCell {
|
export interface HeadCell {
|
||||||
id: string;
|
id: string;
|
||||||
label: string | ReactNode;
|
label: string | ReactNode;
|
||||||
|
info?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnhancedHeaderTableProps {
|
export interface EnhancedHeaderTableProps {
|
||||||
numSelected: number;
|
|
||||||
onRequestSort: (event: MouseEvent<unknown>, property: keyof Data) => void;
|
onRequestSort: (event: MouseEvent<unknown>, property: keyof Data) => void;
|
||||||
onSelectAllClick: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
order: Order;
|
order: Order;
|
||||||
orderBy: string;
|
orderBy: string;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Containers, DefaultActiveTab, Tabs, TSDBStatus } from "./types";
|
import { Containers, Tabs, TSDBStatus } from "./types";
|
||||||
import { useRef } from "preact/compat";
|
import { useRef } from "preact/compat";
|
||||||
import { HeadCell } from "./Table/types";
|
import { HeadCell } from "./Table/types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
tabs: Tabs;
|
tabs: Tabs;
|
||||||
containerRefs: Containers<HTMLDivElement>;
|
containerRefs: Containers<HTMLDivElement>;
|
||||||
defaultActiveTab: DefaultActiveTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AppConfigurator {
|
export default class AppConfigurator {
|
||||||
|
@ -15,6 +14,7 @@ export default class AppConfigurator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tsdbStatus = this.defaultTSDBStatus;
|
this.tsdbStatus = this.defaultTSDBStatus;
|
||||||
this.tabsNames = ["table", "graph"];
|
this.tabsNames = ["table", "graph"];
|
||||||
|
this.getDefaultState = this.getDefaultState.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
set tsdbStatusData(tsdbStatus: TSDBStatus) {
|
set tsdbStatusData(tsdbStatus: TSDBStatus) {
|
||||||
|
@ -29,6 +29,7 @@ export default class AppConfigurator {
|
||||||
return {
|
return {
|
||||||
totalSeries: 0,
|
totalSeries: 0,
|
||||||
totalLabelValuePairs: 0,
|
totalLabelValuePairs: 0,
|
||||||
|
totalSeriesByAll: 0,
|
||||||
seriesCountByMetricName: [],
|
seriesCountByMetricName: [],
|
||||||
seriesCountByLabelName: [],
|
seriesCountByLabelName: [],
|
||||||
seriesCountByFocusLabelValue: [],
|
seriesCountByFocusLabelValue: [],
|
||||||
|
@ -37,22 +38,26 @@ export default class AppConfigurator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
keys(focusLabel: string | null): string[] {
|
keys(match?: string | null, focusLabel?: string | null): string[] {
|
||||||
|
const isMetric = match && /__name__=".+"/.test(match);
|
||||||
|
const isLabel = match && /{.+=".+"}/g.test(match);
|
||||||
|
const isMetricWithLabel = match && /__name__=".+", .+!=""/g.test(match);
|
||||||
|
|
||||||
let keys: string[] = [];
|
let keys: string[] = [];
|
||||||
if (focusLabel) {
|
if (focusLabel || isMetricWithLabel) {
|
||||||
keys = keys.concat("seriesCountByFocusLabelValue");
|
keys = keys.concat("seriesCountByFocusLabelValue");
|
||||||
|
} else if (isMetric) {
|
||||||
|
keys = keys.concat("labelValueCountByLabelName");
|
||||||
|
} else if (isLabel) {
|
||||||
|
keys = keys.concat("seriesCountByMetricName", "seriesCountByLabelName");
|
||||||
|
} else {
|
||||||
|
keys = keys.concat("seriesCountByMetricName", "seriesCountByLabelName", "seriesCountByLabelValuePair");
|
||||||
}
|
}
|
||||||
keys = keys.concat(
|
|
||||||
"seriesCountByMetricName",
|
|
||||||
"seriesCountByLabelName",
|
|
||||||
"seriesCountByLabelValuePair",
|
|
||||||
"labelValueCountByLabelName",
|
|
||||||
);
|
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultState(): AppState {
|
getDefaultState(match?: string | null, label?: string | null): AppState {
|
||||||
return this.keys("job").reduce((acc, cur) => {
|
return this.keys(match, label).reduce((acc, cur) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
tabs: {
|
tabs: {
|
||||||
|
@ -63,15 +68,10 @@ export default class AppConfigurator {
|
||||||
...acc.containerRefs,
|
...acc.containerRefs,
|
||||||
[cur]: useRef<HTMLDivElement>(null),
|
[cur]: useRef<HTMLDivElement>(null),
|
||||||
},
|
},
|
||||||
defaultActiveTab: {
|
|
||||||
...acc.defaultActiveTab,
|
|
||||||
[cur]: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
tabs: {} as Tabs,
|
tabs: {} as Tabs,
|
||||||
containerRefs: {} as Containers<HTMLDivElement>,
|
containerRefs: {} as Containers<HTMLDivElement>,
|
||||||
defaultActiveTab: {} as DefaultActiveTab,
|
|
||||||
} as AppState);
|
} as AppState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +85,53 @@ export default class AppConfigurator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get sectionsTips(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
seriesCountByMetricName: `
|
||||||
|
<p>
|
||||||
|
This table returns a list of metrics with the highest cardinality.
|
||||||
|
The cardinality of a metric is the number of time series associated with that metric,
|
||||||
|
where each time series is defined as a unique combination of key-value label pairs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When looking to reduce the number of active series in your data source,
|
||||||
|
you can start by inspecting individual metrics with high cardinality
|
||||||
|
(i.e. that have lots of active time series associated with them),
|
||||||
|
since that single metric contributes a large fraction of the series that make up your total series count.
|
||||||
|
</p>`,
|
||||||
|
seriesCountByLabelName: `
|
||||||
|
<p>
|
||||||
|
This table returns a list of the labels with the highest number of series.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use this table to identify labels that are storing dimensions with high cardinality
|
||||||
|
(many different label values).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It is recommended to choose labels such that they have a finite set of values,
|
||||||
|
since every unique combination of key-value label pairs creates a new time series
|
||||||
|
and therefore can dramatically increase the number of time series in your system.
|
||||||
|
</p>`,
|
||||||
|
seriesCountByFocusLabelValue: `
|
||||||
|
<p>
|
||||||
|
This table returns a list of unique label values per selected label.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use this table to identify label values that are storing per each selected series.
|
||||||
|
</p>`,
|
||||||
|
labelValueCountByLabelName: "",
|
||||||
|
seriesCountByLabelValuePair: `
|
||||||
|
<p>
|
||||||
|
This table returns a list of the label values pairs with the highest number of series.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use this table to identify unique label values pairs. This helps to identify same labels
|
||||||
|
is applied to count timeseries in your system, since every unique combination of key-value label pairs
|
||||||
|
creates a new time series and therefore can dramatically increase the number of time series in your system
|
||||||
|
</p>`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
get tablesHeaders(): Record<string, HeadCell[]> {
|
get tablesHeaders(): Record<string, HeadCell[]> {
|
||||||
return {
|
return {
|
||||||
seriesCountByMetricName: METRIC_NAMES_HEADERS,
|
seriesCountByMetricName: METRIC_NAMES_HEADERS,
|
||||||
|
@ -114,11 +161,12 @@ const METRIC_NAMES_HEADERS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "percentage",
|
id: "percentage",
|
||||||
label: "Percent of series",
|
label: "Share in total",
|
||||||
|
info: "Shows the share of a metric to the total number of series"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
label: "Action",
|
label: "",
|
||||||
}
|
}
|
||||||
] as HeadCell[];
|
] as HeadCell[];
|
||||||
|
|
||||||
|
@ -133,11 +181,12 @@ const LABEL_NAMES_HEADERS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "percentage",
|
id: "percentage",
|
||||||
label: "Percent of series",
|
label: "Share in total",
|
||||||
|
info: "Shows the share of the label to the total number of series"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
label: "Action",
|
label: "",
|
||||||
}
|
}
|
||||||
] as HeadCell[];
|
] as HeadCell[];
|
||||||
|
|
||||||
|
@ -152,12 +201,12 @@ const FOCUS_LABEL_VALUES_HEADERS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "percentage",
|
id: "percentage",
|
||||||
label: "Percent of series",
|
label: "Share in total",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
disablePadding: false,
|
disablePadding: false,
|
||||||
id: "action",
|
id: "action",
|
||||||
label: "Action",
|
label: "",
|
||||||
numeric: false,
|
numeric: false,
|
||||||
}
|
}
|
||||||
] as HeadCell[];
|
] as HeadCell[];
|
||||||
|
@ -173,11 +222,12 @@ export const LABEL_VALUE_PAIRS_HEADERS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "percentage",
|
id: "percentage",
|
||||||
label: "Percent of series",
|
label: "Share in total",
|
||||||
|
info: "Shows the share of the label value pair to the total number of series"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
label: "Action",
|
label: "",
|
||||||
}
|
}
|
||||||
] as HeadCell[];
|
] as HeadCell[];
|
||||||
|
|
||||||
|
@ -192,6 +242,6 @@ export const LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
label: "Action",
|
label: "",
|
||||||
}
|
}
|
||||||
] as HeadCell[];
|
] as HeadCell[];
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import { QueryUpdater } from "./types";
|
import { QueryUpdater } from "./types";
|
||||||
|
|
||||||
export const queryUpdater: QueryUpdater = {
|
export const queryUpdater: QueryUpdater = {
|
||||||
seriesCountByMetricName: (focusLabel: string | null, query: string): string => {
|
seriesCountByMetricName: ({ query }): string => {
|
||||||
return getSeriesSelector("__name__", query);
|
return getSeriesSelector("__name__", query);
|
||||||
},
|
},
|
||||||
seriesCountByLabelName: (focusLabel: string | null, query: string): string => `{${query}!=""}`,
|
seriesCountByLabelName: ({ query }): string => {
|
||||||
seriesCountByFocusLabelValue: (focusLabel: string | null, query: string): string => {
|
return `{${query}!=""}`;
|
||||||
|
},
|
||||||
|
seriesCountByFocusLabelValue: ({ query, focusLabel }): string => {
|
||||||
return getSeriesSelector(focusLabel, query);
|
return getSeriesSelector(focusLabel, query);
|
||||||
},
|
},
|
||||||
seriesCountByLabelValuePair: (focusLabel: string | null, query: string): string => {
|
seriesCountByLabelValuePair: ({ query }): string => {
|
||||||
const a = query.split("=");
|
const a = query.split("=");
|
||||||
const label = a[0];
|
const label = a[0];
|
||||||
const value = a.slice(1).join("=");
|
const value = a.slice(1).join("=");
|
||||||
return getSeriesSelector(label, value);
|
return getSeriesSelector(label, value);
|
||||||
},
|
},
|
||||||
labelValueCountByLabelName: (focusLabel: string | null, query: string): string => `{${query}!=""}`,
|
labelValueCountByLabelName: ({ query, match }): string => {
|
||||||
|
return `${match.replace("}", "")}, ${query}!=""}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSeriesSelector = (label: string | null, value: string): string => {
|
const getSeriesSelector = (label: string | null, value: string): string => {
|
||||||
|
|
|
@ -3,8 +3,10 @@ import { useAppState } from "../../../state/common/StateContext";
|
||||||
import { useEffect, useState } from "preact/compat";
|
import { useEffect, useState } from "preact/compat";
|
||||||
import { CardinalityRequestsParams, getCardinalityInfo } from "../../../api/tsdb";
|
import { CardinalityRequestsParams, getCardinalityInfo } from "../../../api/tsdb";
|
||||||
import { TSDBStatus } from "../types";
|
import { TSDBStatus } from "../types";
|
||||||
import { useCardinalityState } from "../../../state/cardinality/CardinalityStateContext";
|
|
||||||
import AppConfigurator from "../appConfigurator";
|
import AppConfigurator from "../appConfigurator";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { DATE_FORMAT } from "../../../constants/date";
|
||||||
|
|
||||||
export const useFetchQuery = (): {
|
export const useFetchQuery = (): {
|
||||||
fetchUrl?: string[],
|
fetchUrl?: string[],
|
||||||
|
@ -13,33 +15,43 @@ export const useFetchQuery = (): {
|
||||||
appConfigurator: AppConfigurator,
|
appConfigurator: AppConfigurator,
|
||||||
} => {
|
} => {
|
||||||
const appConfigurator = new AppConfigurator();
|
const appConfigurator = new AppConfigurator();
|
||||||
const { topN, extraLabel, match, date, runQuery, focusLabel } = useCardinalityState();
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const match = searchParams.get("match");
|
||||||
|
const focusLabel = searchParams.get("focusLabel");
|
||||||
|
const topN = +(searchParams.get("topN") || 10);
|
||||||
|
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
|
||||||
|
|
||||||
const { serverUrl } = useAppState();
|
const { serverUrl } = useAppState();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<ErrorTypes | string>();
|
const [error, setError] = useState<ErrorTypes | string>();
|
||||||
const [tsdbStatus, setTSDBStatus] = useState<TSDBStatus>(appConfigurator.defaultTSDBStatus);
|
const [tsdbStatus, setTSDBStatus] = useState<TSDBStatus>(appConfigurator.defaultTSDBStatus);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (error) {
|
|
||||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
|
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
|
||||||
if (!serverUrl) return;
|
if (!serverUrl) return;
|
||||||
setError("");
|
setError("");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||||
|
|
||||||
|
const defaultParams = { date: requestParams.date, topN: 0, match: "", focusLabel: "" } as CardinalityRequestsParams;
|
||||||
const url = getCardinalityInfo(serverUrl, requestParams);
|
const url = getCardinalityInfo(serverUrl, requestParams);
|
||||||
|
const urlDefault = getCardinalityInfo(serverUrl, defaultParams);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const resp = await response.json();
|
const resp = await response.json();
|
||||||
|
const responseTotal = await fetch(urlDefault);
|
||||||
|
const respTotals = await responseTotal.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const { data } = resp;
|
const { data } = resp;
|
||||||
setTSDBStatus({ ...data });
|
const { totalSeries } = respTotals.data;
|
||||||
|
const result = { ...data } as TSDBStatus;
|
||||||
|
result.totalSeriesByAll = totalSeries;
|
||||||
|
|
||||||
|
const name = match?.replace(/[{}"]/g, "");
|
||||||
|
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
|
||||||
|
|
||||||
|
setTSDBStatus(result);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setError(resp.error);
|
setError(resp.error);
|
||||||
|
@ -54,8 +66,15 @@ export const useFetchQuery = (): {
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCardinalityInfo({ topN, extraLabel, match, date, focusLabel });
|
fetchCardinalityInfo({ topN, match, date, focusLabel });
|
||||||
}, [serverUrl, runQuery, date]);
|
}, [serverUrl, match, focusLabel, topN, date]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
appConfigurator.tsdbStatusData = tsdbStatus;
|
appConfigurator.tsdbStatusData = tsdbStatus;
|
||||||
return { isLoading, appConfigurator: appConfigurator, error };
|
return { isLoading, appConfigurator: appConfigurator, error };
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useCardinalityState } from "../../../state/cardinality/CardinalityStateContext";
|
|
||||||
import { compactObject } from "../../../utils/object";
|
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
|
|
||||||
export const useSetQueryParams = () => {
|
|
||||||
const { topN, match, date, focusLabel, extraLabel } = useCardinalityState();
|
|
||||||
const [, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const setSearchParamsFromState = () => {
|
|
||||||
const params = compactObject({
|
|
||||||
topN,
|
|
||||||
date,
|
|
||||||
match,
|
|
||||||
extraLabel,
|
|
||||||
focusLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSearchParams(params as Record<string, string>);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(setSearchParamsFromState, [topN, match, date, focusLabel, extraLabel]);
|
|
||||||
useEffect(setSearchParamsFromState, []);
|
|
||||||
};
|
|
|
@ -1,75 +1,48 @@
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC } from "react";
|
||||||
import { useFetchQuery } from "./hooks/useCardinalityFetch";
|
import { useFetchQuery } from "./hooks/useCardinalityFetch";
|
||||||
import { queryUpdater } from "./helpers";
|
import { queryUpdater } from "./helpers";
|
||||||
import { Data } from "./Table/types";
|
import { Data } from "./Table/types";
|
||||||
import CardinalityConfigurator from "./CardinalityConfigurator/CardinalityConfigurator";
|
import CardinalityConfigurator from "./CardinalityConfigurator/CardinalityConfigurator";
|
||||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||||
import { useCardinalityDispatch, useCardinalityState } from "../../state/cardinality/CardinalityStateContext";
|
|
||||||
import MetricsContent from "./MetricsContent/MetricsContent";
|
import MetricsContent from "./MetricsContent/MetricsContent";
|
||||||
import { DefaultActiveTab, Tabs, TSDBStatus, Containers } from "./types";
|
import { Tabs, TSDBStatus, Containers } from "./types";
|
||||||
import { useSetQueryParams } from "./hooks/useSetQueryParams";
|
|
||||||
import Alert from "../../components/Main/Alert/Alert";
|
import Alert from "../../components/Main/Alert/Alert";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../hooks/useDeviceDetect";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
TipCardinalityOfLabel,
|
||||||
|
TipCardinalityOfSingle,
|
||||||
|
TipHighNumberOfSeries,
|
||||||
|
TipHighNumberOfValues
|
||||||
|
} from "./CardinalityTips";
|
||||||
|
|
||||||
const spinnerMessage = `Please wait while cardinality stats is calculated.
|
const spinnerMessage = `Please wait while cardinality stats is calculated.
|
||||||
This may take some time if the db contains big number of time series.`;
|
This may take some time if the db contains big number of time series.`;
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
const { topN, match, date, focusLabel } = useCardinalityState();
|
|
||||||
const cardinalityDispatch = useCardinalityDispatch();
|
|
||||||
useSetQueryParams();
|
|
||||||
|
|
||||||
const configError = "";
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [query, setQuery] = useState(match || "");
|
const showTips = searchParams.get("tips") || "";
|
||||||
const [queryHistoryIndex, setQueryHistoryIndex] = useState(0);
|
const match = searchParams.get("match") || "";
|
||||||
const [queryHistory, setQueryHistory] = useState<string[]>([]);
|
const focusLabel = searchParams.get("focusLabel") || "";
|
||||||
|
|
||||||
const onRunQuery = () => {
|
|
||||||
setQueryHistory(prev => [...prev, query]);
|
|
||||||
setQueryHistoryIndex(prev => prev + 1);
|
|
||||||
cardinalityDispatch({ type: "SET_MATCH", payload: query });
|
|
||||||
cardinalityDispatch({ type: "RUN_QUERY" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSetHistory = (step: number) => {
|
|
||||||
const newIndexHistory = queryHistoryIndex + step;
|
|
||||||
if (newIndexHistory < 0 || newIndexHistory >= queryHistory.length) return;
|
|
||||||
setQueryHistoryIndex(newIndexHistory);
|
|
||||||
setQuery(queryHistory[newIndexHistory]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTopNChange = (value: string) => {
|
|
||||||
cardinalityDispatch({ type: "SET_TOP_N", payload: +value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFocusLabelChange = (value: string) => {
|
|
||||||
cardinalityDispatch({ type: "SET_FOCUS_LABEL", payload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isLoading, appConfigurator, error } = useFetchQuery();
|
const { isLoading, appConfigurator, error } = useFetchQuery();
|
||||||
const [stateTabs, setTab] = useState(appConfigurator.defaultState.defaultActiveTab);
|
const { tsdbStatusData, getDefaultState, tablesHeaders, sectionsTips } = appConfigurator;
|
||||||
const { tsdbStatusData, defaultState, tablesHeaders } = appConfigurator;
|
const defaultState = getDefaultState(match, focusLabel);
|
||||||
const handleTabChange = (newValue: string, tabId: string) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
setTab({ ...stateTabs, [tabId]: +newValue });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterClick = (key: string) => (name: string) => {
|
const handleFilterClick = (key: string) => (query: string) => {
|
||||||
const query = queryUpdater[key](focusLabel, name);
|
const value = queryUpdater[key]({ query, focusLabel, match });
|
||||||
setQuery(query);
|
searchParams.set("match", value);
|
||||||
setQueryHistory(prev => [...prev, query]);
|
|
||||||
setQueryHistoryIndex(prev => prev + 1);
|
|
||||||
cardinalityDispatch({ type: "SET_MATCH", payload: query });
|
|
||||||
let newFocusLabel = "";
|
|
||||||
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
|
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
|
||||||
newFocusLabel = name;
|
searchParams.set("focusLabel", query);
|
||||||
}
|
}
|
||||||
cardinalityDispatch({ type: "SET_FOCUS_LABEL", payload: newFocusLabel });
|
if (key == "seriesCountByFocusLabelValue") {
|
||||||
cardinalityDispatch({ type: "RUN_QUERY" });
|
searchParams.set("focusLabel", "");
|
||||||
|
}
|
||||||
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -81,35 +54,33 @@ const Index: FC = () => {
|
||||||
>
|
>
|
||||||
{isLoading && <Spinner message={spinnerMessage}/>}
|
{isLoading && <Spinner message={spinnerMessage}/>}
|
||||||
<CardinalityConfigurator
|
<CardinalityConfigurator
|
||||||
error={configError}
|
|
||||||
query={query}
|
|
||||||
topN={topN}
|
|
||||||
date={date}
|
|
||||||
match={match}
|
|
||||||
totalSeries={tsdbStatusData.totalSeries}
|
totalSeries={tsdbStatusData.totalSeries}
|
||||||
|
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
|
||||||
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
||||||
focusLabel={focusLabel}
|
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
onSetQuery={setQuery}
|
|
||||||
onSetHistory={onSetHistory}
|
|
||||||
onTopNChange={onTopNChange}
|
|
||||||
onFocusLabelChange={onFocusLabelChange}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showTips && (
|
||||||
|
<div className="vm-cardinality-panel-tips">
|
||||||
|
{!match && !focusLabel && <TipHighNumberOfSeries/>}
|
||||||
|
{match && !focusLabel && <TipCardinalityOfSingle/>}
|
||||||
|
{!match && !focusLabel && <TipHighNumberOfValues/>}
|
||||||
|
{focusLabel && <TipCardinalityOfLabel/>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && <Alert variant="error">{error}</Alert>}
|
{error && <Alert variant="error">{error}</Alert>}
|
||||||
|
|
||||||
{appConfigurator.keys(focusLabel).map((keyName) => (
|
{appConfigurator.keys(match, focusLabel).map((keyName) => (
|
||||||
<MetricsContent
|
<MetricsContent
|
||||||
key={keyName}
|
key={keyName}
|
||||||
sectionTitle={appConfigurator.sectionsTitles(focusLabel)[keyName]}
|
sectionTitle={appConfigurator.sectionsTitles(focusLabel)[keyName]}
|
||||||
activeTab={stateTabs[keyName as keyof DefaultActiveTab]}
|
tip={sectionsTips[keyName]}
|
||||||
rows={tsdbStatusData[keyName as keyof TSDBStatus] as unknown as Data[]}
|
rows={tsdbStatusData[keyName as keyof TSDBStatus] as unknown as Data[]}
|
||||||
onChange={handleTabChange}
|
|
||||||
onActionClick={handleFilterClick(keyName)}
|
onActionClick={handleFilterClick(keyName)}
|
||||||
tabs={defaultState.tabs[keyName as keyof Tabs]}
|
tabs={defaultState.tabs[keyName as keyof Tabs]}
|
||||||
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
|
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
|
||||||
totalSeries={appConfigurator.totalSeries(keyName)}
|
totalSeries={appConfigurator.totalSeries(keyName)}
|
||||||
tabId={keyName}
|
|
||||||
tableHeaderCells={tablesHeaders[keyName]}
|
tableHeaderCells={tablesHeaders[keyName]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -5,7 +5,17 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
|
|
||||||
&_mobile {
|
&_mobile, &_mobile &-tips {
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-tips {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: $padding-medium;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { MutableRef } from "preact/hooks";
|
||||||
export interface TSDBStatus {
|
export interface TSDBStatus {
|
||||||
totalSeries: number;
|
totalSeries: number;
|
||||||
totalLabelValuePairs: number;
|
totalLabelValuePairs: number;
|
||||||
|
totalSeriesByAll: number,
|
||||||
seriesCountByMetricName: TopHeapEntry[];
|
seriesCountByMetricName: TopHeapEntry[];
|
||||||
seriesCountByLabelName: TopHeapEntry[];
|
seriesCountByLabelName: TopHeapEntry[];
|
||||||
seriesCountByFocusLabelValue: TopHeapEntry[];
|
seriesCountByFocusLabelValue: TopHeapEntry[];
|
||||||
|
@ -12,11 +13,17 @@ export interface TSDBStatus {
|
||||||
|
|
||||||
export interface TopHeapEntry {
|
export interface TopHeapEntry {
|
||||||
name: string;
|
name: string;
|
||||||
count: number;
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryUpdaterArgs {
|
||||||
|
query: string;
|
||||||
|
focusLabel: string;
|
||||||
|
match: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryUpdater = {
|
export type QueryUpdater = {
|
||||||
[key: string]: (focusLabel: string | null, query: string) => string,
|
[key: string]: (args: QueryUpdaterArgs) => string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tabs {
|
export interface Tabs {
|
||||||
|
@ -34,11 +41,3 @@ export interface Containers<T> {
|
||||||
seriesCountByLabelValuePair: MutableRef<T>;
|
seriesCountByLabelValuePair: MutableRef<T>;
|
||||||
labelValueCountByLabelName: MutableRef<T>;
|
labelValueCountByLabelName: MutableRef<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultActiveTab {
|
|
||||||
seriesCountByMetricName: number;
|
|
||||||
seriesCountByLabelName: number;
|
|
||||||
seriesCountByFocusLabelValue: number;
|
|
||||||
seriesCountByLabelValuePair: number;
|
|
||||||
labelValueCountByLabelName: number;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import React, { createContext, FC, useContext, useMemo, useReducer } from "preact/compat";
|
|
||||||
import { Action, CardinalityState, initialState, reducer } from "./reducer";
|
|
||||||
import { Dispatch } from "react";
|
|
||||||
|
|
||||||
type CardinalityStateContextType = { state: CardinalityState, dispatch: Dispatch<Action> };
|
|
||||||
|
|
||||||
export const CardinalityStateContext = createContext<CardinalityStateContextType>({} as CardinalityStateContextType);
|
|
||||||
|
|
||||||
export const useCardinalityState = (): CardinalityState => useContext(CardinalityStateContext).state;
|
|
||||||
export const useCardinalityDispatch = (): Dispatch<Action> => useContext(CardinalityStateContext).dispatch;
|
|
||||||
|
|
||||||
export const CardinalityStateProvider: FC = ({ children }) => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
|
||||||
|
|
||||||
const contextValue = useMemo(() => {
|
|
||||||
return { state, dispatch };
|
|
||||||
}, [state, dispatch]);
|
|
||||||
|
|
||||||
return <CardinalityStateContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</CardinalityStateContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { getQueryStringValue } from "../../utils/query-string";
|
|
||||||
import { DATE_FORMAT } from "../../constants/date";
|
|
||||||
|
|
||||||
export interface CardinalityState {
|
|
||||||
runQuery: number,
|
|
||||||
topN: number
|
|
||||||
date: string | null
|
|
||||||
match: string | null
|
|
||||||
extraLabel: string | null
|
|
||||||
focusLabel: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Action =
|
|
||||||
| { type: "SET_TOP_N", payload: number }
|
|
||||||
| { type: "SET_DATE", payload: string | null }
|
|
||||||
| { type: "SET_MATCH", payload: string | null }
|
|
||||||
| { type: "SET_EXTRA_LABEL", payload: string | null }
|
|
||||||
| { type: "SET_FOCUS_LABEL", payload: string | null }
|
|
||||||
| { type: "RUN_QUERY" }
|
|
||||||
|
|
||||||
|
|
||||||
export const initialState: CardinalityState = {
|
|
||||||
runQuery: 0,
|
|
||||||
topN: getQueryStringValue("topN", 10) as number,
|
|
||||||
date: getQueryStringValue("date", dayjs().tz().format(DATE_FORMAT)) as string,
|
|
||||||
focusLabel: getQueryStringValue("focusLabel", "") as string,
|
|
||||||
match: getQueryStringValue("match", "") as string,
|
|
||||||
extraLabel: getQueryStringValue("extra_label", "") as string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function reducer(state: CardinalityState, action: Action): CardinalityState {
|
|
||||||
switch (action.type) {
|
|
||||||
case "SET_TOP_N":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
topN: action.payload
|
|
||||||
};
|
|
||||||
case "SET_DATE":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
date: action.payload
|
|
||||||
};
|
|
||||||
case "SET_MATCH":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
match: action.payload
|
|
||||||
};
|
|
||||||
case "SET_EXTRA_LABEL":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
extraLabel: action.payload
|
|
||||||
};
|
|
||||||
case "SET_FOCUS_LABEL":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
focusLabel: action.payload,
|
|
||||||
};
|
|
||||||
case "RUN_QUERY":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
runQuery: state.runQuery + 1
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue