mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-10 15:14:09 +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 {
|
||||
topN: number,
|
||||
extraLabel: string | null,
|
||||
match: string | null,
|
||||
date: 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 { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext";
|
||||
import React, { FC, useEffect, useMemo, useRef } from "preact/compat";
|
||||
import dayjs from "dayjs";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons";
|
||||
|
@ -8,21 +7,28 @@ import { getAppModeEnable } from "../../../utils/app-mode";
|
|||
import { DATE_FORMAT } from "../../../constants/date";
|
||||
import DatePicker from "../../Main/DatePicker/DatePicker";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const CardinalityDatePicker: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { date } = useCardinalityState();
|
||||
const cardinalityDispatch = useCardinalityDispatch();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const date = searchParams.get("date") || dayjs().tz().format(DATE_FORMAT);
|
||||
|
||||
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
|
||||
|
||||
const handleChangeDate = (val: string) => {
|
||||
cardinalityDispatch({ type: "SET_DATE", payload: val });
|
||||
searchParams.set("date", val);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleChangeDate(date);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={buttonRef}>
|
||||
|
|
|
@ -387,3 +387,14 @@ export const TuneIcon = () => (
|
|||
></path>
|
||||
</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 { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
|
||||
import { GraphStateProvider } from "../state/graph/GraphStateContext";
|
||||
import { CardinalityStateProvider } from "../state/cardinality/CardinalityStateContext";
|
||||
import { TopQueriesStateProvider } from "../state/topQueries/TopQueriesStateContext";
|
||||
import { SnackbarProvider } from "./Snackbar";
|
||||
|
||||
|
@ -16,7 +15,6 @@ const providers = [
|
|||
QueryStateProvider,
|
||||
CustomPanelStateProvider,
|
||||
GraphStateProvider,
|
||||
CardinalityStateProvider,
|
||||
TopQueriesStateProvider,
|
||||
SnackbarProvider,
|
||||
DashboardsStateProvider
|
||||
|
|
|
@ -1,96 +1,76 @@
|
|||
import React, { FC, useMemo } from "react";
|
||||
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
||||
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 { PlayIcon, QuestionIcon, RestartIcon, TipIcon, WikiIcon } from "../../../components/Main/Icons";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import TextField from "../../../components/Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
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 {
|
||||
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 CardinalityConfigurator: FC<CardinalityTotalsProps> = (props) => {
|
||||
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 = () => {
|
||||
queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" });
|
||||
const handleTopNChange = (val: string) => {
|
||||
const num = +val;
|
||||
setTopN(isNaN(num) ? 0 : num);
|
||||
};
|
||||
|
||||
const handleArrowUp = () => {
|
||||
onSetHistory(-1);
|
||||
const handleRunQuery = () => {
|
||||
searchParams.set("match", match);
|
||||
searchParams.set("topN", topN.toString());
|
||||
searchParams.set("focusLabel", focusLabel);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
const handleArrowDown = () => {
|
||||
onSetHistory(1);
|
||||
const handleResetQuery = () => {
|
||||
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
|
||||
className={classNames({
|
||||
"vm-cardinality-configurator": true,
|
||||
"vm-cardinality-configurator_mobile": isMobile,
|
||||
"vm-block": true,
|
||||
"vm-block_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
<div className="vm-cardinality-configurator-controls">
|
||||
<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
|
||||
label="Number of entries per table"
|
||||
type="number"
|
||||
value={topN}
|
||||
error={errorTopN}
|
||||
onChange={onTopNChange}
|
||||
label="Time series selector"
|
||||
type="string"
|
||||
value={match}
|
||||
onChange={setMatch}
|
||||
onEnter={handleRunQuery}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-cardinality-configurator-controls__item">
|
||||
|
@ -98,41 +78,36 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|||
label="Focus label"
|
||||
type="text"
|
||||
value={focusLabel || ""}
|
||||
onChange={onFocusLabelChange}
|
||||
onChange={setFocusLabel}
|
||||
onEnter={handleRunQuery}
|
||||
endIcon={(
|
||||
<Tooltip
|
||||
title={(
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
>
|
||||
<InfoIcon/>
|
||||
<QuestionIcon/>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-cardinality-configurator-additional">
|
||||
<Switch
|
||||
label={"Autocomplete"}
|
||||
value={autocomplete}
|
||||
onChange={onChangeAutocomplete}
|
||||
/>
|
||||
</div>
|
||||
<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 className="vm-cardinality-configurator-controls__item vm-cardinality-configurator-controls__item_limit">
|
||||
<TextField
|
||||
label="Limit entries"
|
||||
type="number"
|
||||
value={topN}
|
||||
error={errorTopN}
|
||||
onChange={handleTopNChange}
|
||||
onEnter={handleRunQuery}
|
||||
/>
|
||||
</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
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
|
@ -140,25 +115,33 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
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
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
startIcon={<PlayIcon/>}
|
||||
onClick={onRunQuery}
|
||||
fullWidth
|
||||
>
|
||||
Execute Query
|
||||
</Button>
|
||||
|
||||
<div className="vm-cardinality-configurator-bottom__execute">
|
||||
<Tooltip title={showTips ? "Hide tips" : "Show tips"}>
|
||||
<Button
|
||||
variant="text"
|
||||
color={showTips ? "warning" : "gray"}
|
||||
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>;
|
||||
};
|
||||
|
|
|
@ -9,58 +9,65 @@
|
|||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 $padding-medium;
|
||||
gap: $padding-small $padding-medium;
|
||||
|
||||
&__query {
|
||||
flex-grow: 8;
|
||||
flex-grow: 10;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
flex-grow: 2;
|
||||
|
||||
&-additional {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $padding-small;
|
||||
&_limit {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: $color-text-disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-global;
|
||||
width: 100%;
|
||||
|
||||
&__docs {
|
||||
&-helpful {
|
||||
display: flex;
|
||||
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 {
|
||||
justify-content: space-between;
|
||||
&__execute {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
&_mobile &-bottom {
|
||||
justify-content: center;
|
||||
|
||||
&-helpful {
|
||||
flex-grow: 1;
|
||||
font-size: $font-size;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
&__execute {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
button:nth-child(3) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 EnhancedTable from "../Table/Table";
|
||||
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 { MutableRef } from "preact/hooks";
|
||||
import Tabs from "../../../components/Main/Tabs/Tabs";
|
||||
import { useMemo } from "preact/compat";
|
||||
import { ChartIcon, TableIcon } from "../../../components/Main/Icons";
|
||||
import { useMemo, useState } from "preact/compat";
|
||||
import { ChartIcon, InfoIcon, TableIcon } from "../../../components/Main/Icons";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import SimpleBarChart from "../../../components/Chart/SimpleBarChart/SimpleBarChart";
|
||||
|
||||
interface MetricsProperties {
|
||||
rows: Data[];
|
||||
activeTab: number;
|
||||
onChange: (newValue: string, tabId: string) => void;
|
||||
onActionClick: (name: string) => void;
|
||||
tabs: string[];
|
||||
chartContainer: MutableRef<HTMLDivElement> | undefined;
|
||||
totalSeries: number,
|
||||
tabId: string;
|
||||
sectionTitle: string;
|
||||
tip?: string;
|
||||
tableHeaderCells: HeadCell[];
|
||||
}
|
||||
|
||||
const MetricsContent: FC<MetricsProperties> = ({
|
||||
rows,
|
||||
activeTab,
|
||||
onChange,
|
||||
tabs: tabsProps,
|
||||
tabs: tabsProps = [],
|
||||
chartContainer,
|
||||
totalSeries,
|
||||
tabId,
|
||||
onActionClick,
|
||||
sectionTitle,
|
||||
tip,
|
||||
tableHeaderCells,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const [activeTab, setActiveTab] = useState("table");
|
||||
|
||||
const tableCells = (row: Data) => (
|
||||
<TableCells
|
||||
|
@ -48,15 +45,11 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
);
|
||||
|
||||
const tabs = useMemo(() => tabsProps.map((t, i) => ({
|
||||
value: String(i),
|
||||
value: t,
|
||||
label: t,
|
||||
icon: i === 0 ? <TableIcon /> : <ChartIcon />
|
||||
})), [tabsProps]);
|
||||
|
||||
const handleChangeTab = (newValue: string) => {
|
||||
onChange(newValue, tabId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -69,47 +62,53 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
<div className="vm-metrics-content-header vm-section-header">
|
||||
<h5
|
||||
className={classNames({
|
||||
"vm-metrics-content-header__title": true,
|
||||
"vm-section-header__title": true,
|
||||
"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">
|
||||
<Tabs
|
||||
activeItem={String(activeTab)}
|
||||
activeItem={activeTab}
|
||||
items={tabs}
|
||||
onChange={handleChangeTab}
|
||||
onChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={chartContainer}
|
||||
className={classNames({
|
||||
"vm-metrics-content__table": true,
|
||||
"vm-metrics-content__table_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{activeTab === 0 && (
|
||||
|
||||
{activeTab === "table" && (
|
||||
<div
|
||||
ref={chartContainer}
|
||||
className={classNames({
|
||||
"vm-metrics-content__table": true,
|
||||
"vm-metrics-content__table_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<EnhancedTable
|
||||
rows={rows}
|
||||
headerCells={tableHeaderCells}
|
||||
defaultSortColumn={"value"}
|
||||
tableCells={tableCells}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 1 && (
|
||||
<BarChart
|
||||
data={[
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
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>
|
||||
)}
|
||||
{activeTab === "graph" && (
|
||||
<div className="vm-metrics-content__chart">
|
||||
<SimpleBarChart data={rows.map(({ name, value }) => ({ name, value }))}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,33 @@
|
|||
.vm-metrics-content {
|
||||
&-header {
|
||||
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 {
|
||||
|
@ -30,4 +57,8 @@
|
|||
&_mobile &__table {
|
||||
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 { ChangeEvent, MouseEvent } from "react";
|
||||
import { MouseEvent } from "react";
|
||||
import { Data, Order, TableProps, } from "./types";
|
||||
import { EnhancedTableHead } from "./TableHead";
|
||||
import { getComparator, stableSort } from "./helpers";
|
||||
import classNames from "classnames";
|
||||
|
||||
const EnhancedTable: FC<TableProps> = ({
|
||||
rows,
|
||||
|
@ -14,7 +13,6 @@ const EnhancedTable: FC<TableProps> = ({
|
|||
|
||||
const [order, setOrder] = useState<Order>("desc");
|
||||
const [orderBy, setOrderBy] = useState<keyof Data>(defaultSortColumn);
|
||||
const [selected, setSelected] = useState<readonly string[]>([]);
|
||||
|
||||
const handleRequestSort = (
|
||||
event: MouseEvent<unknown>,
|
||||
|
@ -25,45 +23,13 @@ const EnhancedTable: FC<TableProps> = ({
|
|||
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));
|
||||
|
||||
return (
|
||||
<table className="vm-table">
|
||||
<EnhancedTableHead
|
||||
numSelected={selected.length}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onSelectAllClick={handleSelectAllClick}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={rows.length}
|
||||
headerCells={headerCells}
|
||||
|
@ -71,12 +37,8 @@ const EnhancedTable: FC<TableProps> = ({
|
|||
<tbody className="vm-table-header">
|
||||
{sortedData.map((row) => (
|
||||
<tr
|
||||
className={classNames({
|
||||
"vm-table__row": true,
|
||||
"vm-table__row_selected": isSelected(row.name)
|
||||
})}
|
||||
className="vm-table__row"
|
||||
key={row.name}
|
||||
onClick={handleClick(row.name)}
|
||||
>
|
||||
{tableCells(row)}
|
||||
</tr>
|
||||
|
|
|
@ -23,7 +23,12 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
|
|||
className="vm-table-cell"
|
||||
key={row.name}
|
||||
>
|
||||
{row.name}
|
||||
<span
|
||||
className="vm-link vm-link_colored"
|
||||
onClick={handleActionClick}
|
||||
>
|
||||
{row.name}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="vm-table-cell"
|
||||
|
|
|
@ -2,7 +2,8 @@ import { MouseEvent } from "react";
|
|||
import React from "preact/compat";
|
||||
import { Data, EnhancedHeaderTableProps } from "./types";
|
||||
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) {
|
||||
const { order, orderBy, onRequestSort, headerCells } = props;
|
||||
|
@ -24,7 +25,13 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
|
|||
onClick={createSortHandler(headCell.id as keyof Data)}
|
||||
>
|
||||
<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" && (
|
||||
<div
|
||||
className={classNames({
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { ChangeEvent, MouseEvent, ReactNode } from "react";
|
||||
import { MouseEvent, ReactNode } from "react";
|
||||
|
||||
export type Order = "asc" | "desc";
|
||||
|
||||
export interface HeadCell {
|
||||
id: string;
|
||||
label: string | ReactNode;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
export interface EnhancedHeaderTableProps {
|
||||
numSelected: number;
|
||||
onRequestSort: (event: MouseEvent<unknown>, property: keyof Data) => void;
|
||||
onSelectAllClick: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
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 { HeadCell } from "./Table/types";
|
||||
|
||||
interface AppState {
|
||||
tabs: Tabs;
|
||||
containerRefs: Containers<HTMLDivElement>;
|
||||
defaultActiveTab: DefaultActiveTab,
|
||||
}
|
||||
|
||||
export default class AppConfigurator {
|
||||
|
@ -15,6 +14,7 @@ export default class AppConfigurator {
|
|||
constructor() {
|
||||
this.tsdbStatus = this.defaultTSDBStatus;
|
||||
this.tabsNames = ["table", "graph"];
|
||||
this.getDefaultState = this.getDefaultState.bind(this);
|
||||
}
|
||||
|
||||
set tsdbStatusData(tsdbStatus: TSDBStatus) {
|
||||
|
@ -29,6 +29,7 @@ export default class AppConfigurator {
|
|||
return {
|
||||
totalSeries: 0,
|
||||
totalLabelValuePairs: 0,
|
||||
totalSeriesByAll: 0,
|
||||
seriesCountByMetricName: [],
|
||||
seriesCountByLabelName: [],
|
||||
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[] = [];
|
||||
if (focusLabel) {
|
||||
if (focusLabel || isMetricWithLabel) {
|
||||
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;
|
||||
}
|
||||
|
||||
get defaultState(): AppState {
|
||||
return this.keys("job").reduce((acc, cur) => {
|
||||
getDefaultState(match?: string | null, label?: string | null): AppState {
|
||||
return this.keys(match, label).reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
tabs: {
|
||||
|
@ -63,15 +68,10 @@ export default class AppConfigurator {
|
|||
...acc.containerRefs,
|
||||
[cur]: useRef<HTMLDivElement>(null),
|
||||
},
|
||||
defaultActiveTab: {
|
||||
...acc.defaultActiveTab,
|
||||
[cur]: 0,
|
||||
},
|
||||
};
|
||||
}, {
|
||||
tabs: {} as Tabs,
|
||||
containerRefs: {} as Containers<HTMLDivElement>,
|
||||
defaultActiveTab: {} as DefaultActiveTab,
|
||||
} 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[]> {
|
||||
return {
|
||||
seriesCountByMetricName: METRIC_NAMES_HEADERS,
|
||||
|
@ -114,11 +161,12 @@ const METRIC_NAMES_HEADERS = [
|
|||
},
|
||||
{
|
||||
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",
|
||||
label: "Action",
|
||||
label: "",
|
||||
}
|
||||
] as HeadCell[];
|
||||
|
||||
|
@ -133,11 +181,12 @@ const LABEL_NAMES_HEADERS = [
|
|||
},
|
||||
{
|
||||
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",
|
||||
label: "Action",
|
||||
label: "",
|
||||
}
|
||||
] as HeadCell[];
|
||||
|
||||
|
@ -152,12 +201,12 @@ const FOCUS_LABEL_VALUES_HEADERS = [
|
|||
},
|
||||
{
|
||||
id: "percentage",
|
||||
label: "Percent of series",
|
||||
label: "Share in total",
|
||||
},
|
||||
{
|
||||
disablePadding: false,
|
||||
id: "action",
|
||||
label: "Action",
|
||||
label: "",
|
||||
numeric: false,
|
||||
}
|
||||
] as HeadCell[];
|
||||
|
@ -173,11 +222,12 @@ export const LABEL_VALUE_PAIRS_HEADERS = [
|
|||
},
|
||||
{
|
||||
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",
|
||||
label: "Action",
|
||||
label: "",
|
||||
}
|
||||
] as HeadCell[];
|
||||
|
||||
|
@ -192,6 +242,6 @@ export const LABEL_NAMES_WITH_UNIQUE_VALUES_HEADERS = [
|
|||
},
|
||||
{
|
||||
id: "action",
|
||||
label: "Action",
|
||||
label: "",
|
||||
}
|
||||
] as HeadCell[];
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
import { QueryUpdater } from "./types";
|
||||
|
||||
export const queryUpdater: QueryUpdater = {
|
||||
seriesCountByMetricName: (focusLabel: string | null, query: string): string => {
|
||||
seriesCountByMetricName: ({ query }): string => {
|
||||
return getSeriesSelector("__name__", query);
|
||||
},
|
||||
seriesCountByLabelName: (focusLabel: string | null, query: string): string => `{${query}!=""}`,
|
||||
seriesCountByFocusLabelValue: (focusLabel: string | null, query: string): string => {
|
||||
seriesCountByLabelName: ({ query }): string => {
|
||||
return `{${query}!=""}`;
|
||||
},
|
||||
seriesCountByFocusLabelValue: ({ query, focusLabel }): string => {
|
||||
return getSeriesSelector(focusLabel, query);
|
||||
},
|
||||
seriesCountByLabelValuePair: (focusLabel: string | null, query: string): string => {
|
||||
seriesCountByLabelValuePair: ({ query }): string => {
|
||||
const a = query.split("=");
|
||||
const label = a[0];
|
||||
const value = a.slice(1).join("=");
|
||||
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 => {
|
||||
|
|
|
@ -3,8 +3,10 @@ import { useAppState } from "../../../state/common/StateContext";
|
|||
import { useEffect, useState } from "preact/compat";
|
||||
import { CardinalityRequestsParams, getCardinalityInfo } from "../../../api/tsdb";
|
||||
import { TSDBStatus } from "../types";
|
||||
import { useCardinalityState } from "../../../state/cardinality/CardinalityStateContext";
|
||||
import AppConfigurator from "../appConfigurator";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_FORMAT } from "../../../constants/date";
|
||||
|
||||
export const useFetchQuery = (): {
|
||||
fetchUrl?: string[],
|
||||
|
@ -13,33 +15,43 @@ export const useFetchQuery = (): {
|
|||
appConfigurator: 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 [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
const [tsdbStatus, setTSDBStatus] = useState<TSDBStatus>(appConfigurator.defaultTSDBStatus);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
|
||||
if (!serverUrl) return;
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||
|
||||
const defaultParams = { date: requestParams.date, topN: 0, match: "", focusLabel: "" } as CardinalityRequestsParams;
|
||||
const url = getCardinalityInfo(serverUrl, requestParams);
|
||||
const urlDefault = getCardinalityInfo(serverUrl, defaultParams);
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const resp = await response.json();
|
||||
const responseTotal = await fetch(urlDefault);
|
||||
const respTotals = await responseTotal.json();
|
||||
if (response.ok) {
|
||||
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);
|
||||
} else {
|
||||
setError(resp.error);
|
||||
|
@ -54,8 +66,15 @@ export const useFetchQuery = (): {
|
|||
|
||||
|
||||
useEffect(() => {
|
||||
fetchCardinalityInfo({ topN, extraLabel, match, date, focusLabel });
|
||||
}, [serverUrl, runQuery, date]);
|
||||
fetchCardinalityInfo({ topN, match, date, focusLabel });
|
||||
}, [serverUrl, match, focusLabel, topN, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setTSDBStatus(appConfigurator.defaultTSDBStatus);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
appConfigurator.tsdbStatusData = tsdbStatus;
|
||||
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 { queryUpdater } from "./helpers";
|
||||
import { Data } from "./Table/types";
|
||||
import CardinalityConfigurator from "./CardinalityConfigurator/CardinalityConfigurator";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import { useCardinalityDispatch, useCardinalityState } from "../../state/cardinality/CardinalityStateContext";
|
||||
import MetricsContent from "./MetricsContent/MetricsContent";
|
||||
import { DefaultActiveTab, Tabs, TSDBStatus, Containers } from "./types";
|
||||
import { useSetQueryParams } from "./hooks/useSetQueryParams";
|
||||
import { Tabs, TSDBStatus, Containers } from "./types";
|
||||
import Alert from "../../components/Main/Alert/Alert";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
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.
|
||||
This may take some time if the db contains big number of time series.`;
|
||||
|
||||
const Index: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { topN, match, date, focusLabel } = useCardinalityState();
|
||||
const cardinalityDispatch = useCardinalityDispatch();
|
||||
useSetQueryParams();
|
||||
|
||||
const configError = "";
|
||||
const [query, setQuery] = useState(match || "");
|
||||
const [queryHistoryIndex, setQueryHistoryIndex] = useState(0);
|
||||
const [queryHistory, setQueryHistory] = useState<string[]>([]);
|
||||
|
||||
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 [searchParams, setSearchParams] = useSearchParams();
|
||||
const showTips = searchParams.get("tips") || "";
|
||||
const match = searchParams.get("match") || "";
|
||||
const focusLabel = searchParams.get("focusLabel") || "";
|
||||
|
||||
const { isLoading, appConfigurator, error } = useFetchQuery();
|
||||
const [stateTabs, setTab] = useState(appConfigurator.defaultState.defaultActiveTab);
|
||||
const { tsdbStatusData, defaultState, tablesHeaders } = appConfigurator;
|
||||
const handleTabChange = (newValue: string, tabId: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
setTab({ ...stateTabs, [tabId]: +newValue });
|
||||
};
|
||||
const { tsdbStatusData, getDefaultState, tablesHeaders, sectionsTips } = appConfigurator;
|
||||
const defaultState = getDefaultState(match, focusLabel);
|
||||
|
||||
const handleFilterClick = (key: string) => (name: string) => {
|
||||
const query = queryUpdater[key](focusLabel, name);
|
||||
setQuery(query);
|
||||
setQueryHistory(prev => [...prev, query]);
|
||||
setQueryHistoryIndex(prev => prev + 1);
|
||||
cardinalityDispatch({ type: "SET_MATCH", payload: query });
|
||||
let newFocusLabel = "";
|
||||
const handleFilterClick = (key: string) => (query: string) => {
|
||||
const value = queryUpdater[key]({ query, focusLabel, match });
|
||||
searchParams.set("match", value);
|
||||
if (key === "labelValueCountByLabelName" || key == "seriesCountByLabelName") {
|
||||
newFocusLabel = name;
|
||||
searchParams.set("focusLabel", query);
|
||||
}
|
||||
cardinalityDispatch({ type: "SET_FOCUS_LABEL", payload: newFocusLabel });
|
||||
cardinalityDispatch({ type: "RUN_QUERY" });
|
||||
if (key == "seriesCountByFocusLabelValue") {
|
||||
searchParams.set("focusLabel", "");
|
||||
}
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -81,35 +54,33 @@ const Index: FC = () => {
|
|||
>
|
||||
{isLoading && <Spinner message={spinnerMessage}/>}
|
||||
<CardinalityConfigurator
|
||||
error={configError}
|
||||
query={query}
|
||||
topN={topN}
|
||||
date={date}
|
||||
match={match}
|
||||
totalSeries={tsdbStatusData.totalSeries}
|
||||
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
|
||||
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
|
||||
focusLabel={focusLabel}
|
||||
onRunQuery={onRunQuery}
|
||||
onSetQuery={setQuery}
|
||||
onSetHistory={onSetHistory}
|
||||
onTopNChange={onTopNChange}
|
||||
onFocusLabelChange={onFocusLabelChange}
|
||||
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
|
||||
/>
|
||||
|
||||
{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>}
|
||||
|
||||
{appConfigurator.keys(focusLabel).map((keyName) => (
|
||||
{appConfigurator.keys(match, focusLabel).map((keyName) => (
|
||||
<MetricsContent
|
||||
key={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[]}
|
||||
onChange={handleTabChange}
|
||||
onActionClick={handleFilterClick(keyName)}
|
||||
tabs={defaultState.tabs[keyName as keyof Tabs]}
|
||||
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
|
||||
totalSeries={appConfigurator.totalSeries(keyName)}
|
||||
tabId={keyName}
|
||||
tableHeaderCells={tablesHeaders[keyName]}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -5,7 +5,17 @@
|
|||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
|
||||
&_mobile {
|
||||
&_mobile, &_mobile &-tips {
|
||||
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 {
|
||||
totalSeries: number;
|
||||
totalLabelValuePairs: number;
|
||||
totalSeriesByAll: number,
|
||||
seriesCountByMetricName: TopHeapEntry[];
|
||||
seriesCountByLabelName: TopHeapEntry[];
|
||||
seriesCountByFocusLabelValue: TopHeapEntry[];
|
||||
|
@ -12,11 +13,17 @@ export interface TSDBStatus {
|
|||
|
||||
export interface TopHeapEntry {
|
||||
name: string;
|
||||
count: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface QueryUpdaterArgs {
|
||||
query: string;
|
||||
focusLabel: string;
|
||||
match: string;
|
||||
}
|
||||
|
||||
export type QueryUpdater = {
|
||||
[key: string]: (focusLabel: string | null, query: string) => string,
|
||||
[key: string]: (args: QueryUpdaterArgs) => string,
|
||||
}
|
||||
|
||||
export interface Tabs {
|
||||
|
@ -34,11 +41,3 @@ export interface Containers<T> {
|
|||
seriesCountByLabelValuePair: 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