diff --git a/app/vmui/packages/vmui/src/api/tsdb.ts b/app/vmui/packages/vmui/src/api/tsdb.ts index 6248edabf2..b3277d088a 100644 --- a/app/vmui/packages/vmui/src/api/tsdb.ts +++ b/app/vmui/packages/vmui/src/api/tsdb.ts @@ -1,6 +1,5 @@ export interface CardinalityRequestsParams { topN: number, - extraLabel: string | null, match: string | null, date: string | null, focusLabel: string | null, diff --git a/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/SimpleBarChart.tsx b/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/SimpleBarChart.tsx new file mode 100644 index 0000000000..90a6eed7ea --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/SimpleBarChart.tsx @@ -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; diff --git a/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/style.scss b/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/style.scss new file mode 100644 index 0000000000..a274c1cfe8 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Chart/SimpleBarChart/style.scss @@ -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%); + } + } + } + } +} diff --git a/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx b/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx index ad88363bf0..fa1fe982fc 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx @@ -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}> diff --git a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx index f5abdfd342..bbbb0d0fa4 100644 --- a/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Icons/index.tsx @@ -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> +); diff --git a/app/vmui/packages/vmui/src/contexts/AppContextProvider.tsx b/app/vmui/packages/vmui/src/contexts/AppContextProvider.tsx index 4e5bf9a745..e580e2da08 100644 --- a/app/vmui/packages/vmui/src/contexts/AppContextProvider.tsx +++ b/app/vmui/packages/vmui/src/contexts/AppContextProvider.tsx @@ -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 diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/CardinalityConfigurator.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/CardinalityConfigurator.tsx index 5f3f1a3c58..a7c10aa0c2 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/CardinalityConfigurator.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/CardinalityConfigurator.tsx @@ -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>; }; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/style.scss b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/style.scss index 0250536473..f2699eb566 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/style.scss +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityConfigurator/style.scss @@ -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%; } } } diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/index.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/index.tsx new file mode 100644 index 0000000000..4d4be5d712 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/index.tsx @@ -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> +); diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/style.scss b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/style.scss new file mode 100644 index 0000000000..bd96b7bd30 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTips/style.scss @@ -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); + } + } + } +} diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/CardinalityTotals.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/CardinalityTotals.tsx new file mode 100644 index 0000000000..07a828428a --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/CardinalityTotals.tsx @@ -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; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/style.scss b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/style.scss new file mode 100644 index 0000000000..459cb2c090 --- /dev/null +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/CardinalityTotals/style.scss @@ -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; + } + } +} diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/MetricsContent.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/MetricsContent.tsx index fbe3d4abbf..8210fcfa21 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/MetricsContent.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/MetricsContent.tsx @@ -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> ); }; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/style.scss b/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/style.scss index 461eb81505..42a27aaeb3 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/style.scss +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/MetricsContent/style.scss @@ -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; + } } diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/Table.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/Table.tsx index 9fc7125ffa..d23d5f2b90 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/Table.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/Table.tsx @@ -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> diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableCells/TableCells.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableCells/TableCells.tsx index ae37ab2c4c..120f5722a8 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableCells/TableCells.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableCells/TableCells.tsx @@ -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" diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableHead.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableHead.tsx index b6b815f3ac..65f7912b4f 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableHead.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/TableHead.tsx @@ -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({ diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/types.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/types.ts index a8d3308fc9..07a34bab70 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/types.ts +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/Table/types.ts @@ -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; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/appConfigurator.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/appConfigurator.ts index f53be24ef5..9a938ebf1b 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/appConfigurator.ts +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/appConfigurator.ts @@ -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[]; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/helpers.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/helpers.ts index 930f12f371..b7c6a3e40e 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/helpers.ts +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/helpers.ts @@ -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 => { diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useCardinalityFetch.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useCardinalityFetch.ts index 1bec1c209f..55c126167b 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useCardinalityFetch.ts +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useCardinalityFetch.ts @@ -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 }; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useSetQueryParams.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useSetQueryParams.ts deleted file mode 100644 index abcdf5354c..0000000000 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/hooks/useSetQueryParams.ts +++ /dev/null @@ -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, []); -}; diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx b/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx index 1088a9fc42..c333f0d393 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/index.tsx @@ -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]} /> ))} diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/style.scss b/app/vmui/packages/vmui/src/pages/CardinalityPanel/style.scss index 4c9730e77e..f85dc95eb8 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/style.scss +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/style.scss @@ -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%; + } } diff --git a/app/vmui/packages/vmui/src/pages/CardinalityPanel/types.ts b/app/vmui/packages/vmui/src/pages/CardinalityPanel/types.ts index a3ecadee61..14bd3316e3 100644 --- a/app/vmui/packages/vmui/src/pages/CardinalityPanel/types.ts +++ b/app/vmui/packages/vmui/src/pages/CardinalityPanel/types.ts @@ -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; -} diff --git a/app/vmui/packages/vmui/src/state/cardinality/CardinalityStateContext.tsx b/app/vmui/packages/vmui/src/state/cardinality/CardinalityStateContext.tsx deleted file mode 100644 index d66d3feb02..0000000000 --- a/app/vmui/packages/vmui/src/state/cardinality/CardinalityStateContext.tsx +++ /dev/null @@ -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>; -}; - - diff --git a/app/vmui/packages/vmui/src/state/cardinality/reducer.ts b/app/vmui/packages/vmui/src/state/cardinality/reducer.ts deleted file mode 100644 index 86531cc9ef..0000000000 --- a/app/vmui/packages/vmui/src/state/cardinality/reducer.ts +++ /dev/null @@ -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(); - } -}