vmui: add a comparison of data to the Cardinality Explorer (#4123)

* feat: add button "show today" to date picker

* feat: add comparison with the prev day (#3967)

* vmui/docs: add comparison of data to cardinality page
This commit is contained in:
Yury Molodov 2023-04-25 11:21:57 +02:00 committed by Aliaksandr Valialkin
parent 382a2ff649
commit 3c45256736
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
19 changed files with 227 additions and 59 deletions

View file

@ -1,4 +1,4 @@
@use "../../../../styles/variables" as *;
@use "src/styles/variables" as *;
.vm-tenant-input {
position: relative;

View file

@ -8,6 +8,7 @@ import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import classNames from "classnames";
import MonthsList from "./MonthsList/MonthsList";
import Button from "../../Button/Button";
interface DatePickerProps {
date: Date | Dayjs
@ -29,6 +30,9 @@ const Calendar: FC<DatePickerProps> = ({
const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days);
const [viewDate, setViewDate] = useState(dayjs.tz(date));
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
const today = dayjs().startOf("day").tz();
const viewDateIsToday = today.format() === viewDate.format();
const { isMobile } = useDeviceDetect();
const toggleDisplayYears = () => {
@ -44,6 +48,10 @@ const Calendar: FC<DatePickerProps> = ({
setSelectDate(date);
};
const handleToday = () => {
setViewDate(today);
};
useEffect(() => {
if (selectDate.format() === dayjs.tz(date).format()) return;
onChange(selectDate.format(format));
@ -88,6 +96,17 @@ const Calendar: FC<DatePickerProps> = ({
onChangeViewDate={handleChangeViewDate}
/>
)}
{!viewDateIsToday && (viewType === CalendarTypeView.days) && (
<div className="vm-calendar-footer">
<Button
variant="text"
size="small"
onClick={handleToday}
>
show today
</Button>
</div>
)}
</div>
);
};

View file

@ -11,6 +11,7 @@ interface CalendarBodyProps {
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSelectDate }) => {
const format = "YYYY-MM-DD";
const today = dayjs().tz().startOf("day");
const days: (Dayjs|null)[] = useMemo(() => {
@ -45,10 +46,10 @@ const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSel
"vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.toISOString()) === selectDate.startOf("day").toISOString(),
"vm-calendar-body-cell_day_today": (d && d.toISOString()) === today.toISOString()
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
})}
key={d ? d.toISOString() : i}
key={d ? d.format(format) : i}
onClick={createHandlerSelectDate(d)}
>
{d && d.format("D")}

View file

@ -166,4 +166,10 @@
}
}
}
&-footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
}

View file

@ -10,12 +10,14 @@ import "./style.scss";
export interface CardinalityTotalsProps {
totalSeries: number;
totalSeriesAll: number;
totalSeriesPrev: number;
totalLabelValuePairs: number;
seriesCountByMetricName: TopHeapEntry[];
}
const CardinalityTotals: FC<CardinalityTotalsProps> = ({
totalSeries,
totalSeriesPrev,
totalSeriesAll,
seriesCountByMetricName
}) => {
@ -27,11 +29,14 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
const isMetric = /__name__/.test(match || "");
const progress = seriesCountByMetricName[0]?.value / totalSeriesAll * 100;
const diff = totalSeries - totalSeriesPrev;
const dynamic = Math.abs(diff) / totalSeriesPrev * 100;
const totals = [
{
title: "Total series",
value: totalSeries.toLocaleString("en-US"),
dynamic: !totalSeries || !totalSeriesPrev ? "" : `${dynamic.toFixed(2)}%`,
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.
@ -57,20 +62,33 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
"vm-cardinality-totals_mobile": isMobile
})}
>
{totals.map(({ title, value, info }) => (
{totals.map(({ title, value, info, dynamic }) => (
<div
className="vm-cardinality-totals-card"
key={title}
>
<div className="vm-cardinality-totals-card-header">
<h4 className="vm-cardinality-totals-card__title">
{title}
{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 title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
<div className="vm-cardinality-totals-card__info-icon"><InfoIcon/></div>
</Tooltip>
)}
<h4 className="vm-cardinality-totals-card-header__title">{title}</h4>
</div>
</h4>
<span className="vm-cardinality-totals-card__value">{value}</span>
{!!dynamic && (
<Tooltip title={`in relation to the previous day: ${totalSeriesPrev.toLocaleString("en-US")}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": diff < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diff > 0,
})}
>
{dynamic}
</span>
</Tooltip>
)}
</div>
))}
</div>

View file

@ -5,56 +5,52 @@
flex-wrap: wrap;
align-content: flex-start;
justify-content: flex-start;
gap: $padding-global;
gap: $padding-medium;
flex-grow: 1;
&_mobile {
gap: $padding-small;
gap: $padding-global;
justify-content: center;
}
&-card {
display: flex;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
justify-content: center;
gap: 4px;
gap: $padding-small 4px;
&-header {
&__info-icon {
width: 12px;
display: flex;
align-items: center;
justify-content: center;
color: $color-primary;
}
&__title {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
grid-column: 1/-1;
color: $color-text;
}
&__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;
}
&__tooltip {
max-width: 280px;
white-space: normal;
padding: $padding-small;
line-height: $font-size;
font-size: $font-size;
}
&__value {
font-weight: bold;
color: $color-primary;
font-size: $font-size-medium;
font-size: $font-size-large;
line-height: $font-size;
text-align: center;
}
}
}

View file

@ -18,6 +18,7 @@ interface MetricsProperties {
tabs: string[];
chartContainer: MutableRef<HTMLDivElement> | undefined;
totalSeries: number,
totalSeriesPrev: number,
sectionTitle: string;
tip?: string;
tableHeaderCells: HeadCell[];
@ -28,6 +29,7 @@ const MetricsContent: FC<MetricsProperties> = ({
tabs: tabsProps = [],
chartContainer,
totalSeries,
totalSeriesPrev,
onActionClick,
sectionTitle,
tip,
@ -40,6 +42,7 @@ const MetricsContent: FC<MetricsProperties> = ({
<TableCells
row={row}
totalSeries={totalSeries}
totalSeriesPrev={totalSeriesPrev}
onActionClick={onActionClick}
/>
);

View file

@ -26,7 +26,7 @@ const EnhancedTable: FC<TableProps> = ({
const sortedData = stableSort(rows, getComparator(order, orderBy));
return (
<table className="vm-table">
<table className="vm-table vm-cardinality-panel-table">
<EnhancedTableHead
order={order}
orderBy={orderBy}

View file

@ -4,15 +4,27 @@ import LineProgress from "../../../../components/Main/LineProgress/LineProgress"
import { PlayCircleOutlineIcon } from "../../../../components/Main/Icons";
import Button from "../../../../components/Main/Button/Button";
import Tooltip from "../../../../components/Main/Tooltip/Tooltip";
import classNames from "classnames";
interface CardinalityTableCells {
row: Data,
totalSeries: number;
totalSeriesPrev: number;
onActionClick: (name: string) => void;
}
const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick }) => {
const TableCells: FC<CardinalityTableCells> = ({
row,
totalSeries,
totalSeriesPrev,
onActionClick
}) => {
const progress = totalSeries > 0 ? row.value / totalSeries * 100 : -1;
const progressPrev = totalSeriesPrev > 0 ? row.valuePrev / totalSeriesPrev * 100 : -1;
const hasProgresses = [progress, progressPrev].some(p => p === -1);
const diffPercent = progress - progressPrev;
const relationPrevDay = hasProgresses ? "" : `${diffPercent.toFixed(2)}%`;
const handleActionClick = () => {
onActionClick(row.name);
@ -35,13 +47,42 @@ const TableCells: FC<CardinalityTableCells> = ({ row, totalSeries, onActionClick
key={row.value}
>
{row.value}
{!!row.diff && (
<Tooltip title={`in relation to the previous day: ${row.valuePrev}`}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive": row.diff < 0,
"vm-dynamic-number_negative": row.diff > 0,
})}
>
&nbsp;{row.diff > 0 ? "+" : ""}{row.diff}
</span>
</Tooltip>
)}
</td>
{progress > 0 && (
<td
className="vm-table-cell"
key={row.progressValue}
>
<LineProgress value={progress}/>
<div className="vm-cardinality-panel-table__progress">
<LineProgress value={progress}/>
{relationPrevDay && (
<Tooltip title={"in relation to the previous day"}>
<span
className={classNames({
"vm-dynamic-number": true,
"vm-dynamic-number_positive vm-dynamic-number_down": diffPercent < 0,
"vm-dynamic-number_negative vm-dynamic-number_up": diffPercent > 0,
})}
>
{relationPrevDay}
</span>
</Tooltip>
)}
</div>
</td>
)}
<td

View file

@ -12,7 +12,7 @@ export function EnhancedTableHead(props: EnhancedHeaderTableProps) {
};
return (
<thead className="vm-table-header">
<thead className="vm-table-header vm-cardinality-panel-table__header">
<tr className="vm-table__row vm-table__row_header">
{headerCells.map((headCell) => (
<th

View file

@ -28,6 +28,8 @@ export interface TableProps {
export interface Data {
name: string;
value: number;
diff: number;
valuePrev: number;
progressValue: number;
actions: string;
}

View file

@ -28,8 +28,9 @@ export default class AppConfigurator {
get defaultTSDBStatus(): TSDBStatus {
return {
totalSeries: 0,
totalLabelValuePairs: 0,
totalSeriesPrev: 0,
totalSeriesByAll: 0,
totalLabelValuePairs: 0,
seriesCountByMetricName: [],
seriesCountByLabelName: [],
seriesCountByFocusLabelValue: [],
@ -142,11 +143,11 @@ export default class AppConfigurator {
};
}
totalSeries(keyName: string): number {
totalSeries(keyName: string, prev = false): number {
if (keyName === "labelValueCountByLabelName") {
return -1;
}
return this.tsdbStatus.totalSeries;
return prev ? this.tsdbStatus.totalSeriesPrev : this.tsdbStatus.totalSeries;
}
}

View file

@ -33,24 +33,51 @@ export const useFetchQuery = (): {
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);
const totalParams = {
date: requestParams.date,
topN: 0,
match: "",
focusLabel: ""
} as CardinalityRequestsParams;
const prevDayParams = {
...requestParams,
date: dayjs(requestParams.date).subtract(1, "day").tz().format(DATE_FORMAT),
} as CardinalityRequestsParams;
const urlBase = getCardinalityInfo(serverUrl, requestParams);
const urlPrev = getCardinalityInfo(serverUrl, prevDayParams);
const uslTotal = getCardinalityInfo(serverUrl, totalParams);
const urls = [urlBase, urlPrev, uslTotal];
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;
const { totalSeries } = respTotals.data;
const result = { ...data } as TSDBStatus;
result.totalSeriesByAll = totalSeries;
const responses = await Promise.all(urls.map(url => fetch(url)));
const [resp, respPrev, respTotals] = await Promise.all(responses.map(resp => resp.json()));
if (responses[0].ok) {
const { data: dataTotal } = respTotals;
const prevResult = { ...respPrev.data } as TSDBStatus;
const result = { ...resp.data } as TSDBStatus;
result.totalSeriesByAll = dataTotal?.totalSeries;
result.totalSeriesPrev = prevResult?.totalSeries;
const name = match?.replace(/[{}"]/g, "");
result.seriesCountByLabelValuePair = result.seriesCountByLabelValuePair.filter(s => s.name !== name);
Object.keys(result).forEach(k => {
const key = k as keyof TSDBStatus;
const entries = result[key];
const prevEntries = prevResult[key];
if (Array.isArray(entries) && Array.isArray(prevEntries)) {
entries.forEach((entry) => {
const valuePrev = prevEntries.find(prevEntry => prevEntry.name === entry.name)?.value;
entry.diff = valuePrev ? entry.value - valuePrev : 0;
entry.valuePrev = valuePrev || 0;
});
}
});
setTSDBStatus(result);
setIsLoading(false);
} else {

View file

@ -55,6 +55,7 @@ const CardinalityPanel: FC = () => {
{isLoading && <Spinner message={spinnerMessage}/>}
<CardinalityConfigurator
totalSeries={tsdbStatusData.totalSeries}
totalSeriesPrev={tsdbStatusData.totalSeriesPrev}
totalSeriesAll={tsdbStatusData.totalSeriesByAll}
totalLabelValuePairs={tsdbStatusData.totalLabelValuePairs}
seriesCountByMetricName={tsdbStatusData.seriesCountByMetricName}
@ -80,6 +81,7 @@ const CardinalityPanel: FC = () => {
onActionClick={handleFilterClick(keyName)}
tabs={defaultState.tabs[keyName as keyof Tabs]}
chartContainer={defaultState.containerRefs[keyName as keyof Containers<HTMLDivElement>]}
totalSeriesPrev={appConfigurator.totalSeries(keyName, true)}
totalSeries={appConfigurator.totalSeries(keyName)}
tableHeaderCells={tablesHeaders[keyName]}
/>

View file

@ -18,4 +18,25 @@
flex-grow: 1;
width: 100%;
}
&-table {
&__header {
th:first-child {
width: 60%;
}
th:not(:first-child) {
width: auto;
}
}
&__progress {
display: grid;
grid-template-columns: minmax(200px, 1fr) 70px;
align-items: center;
justify-content: flex-start;
gap: $padding-small;
}
}
}

View file

@ -4,6 +4,7 @@ export interface TSDBStatus {
totalSeries: number;
totalLabelValuePairs: number;
totalSeriesByAll: number,
totalSeriesPrev: number,
seriesCountByMetricName: TopHeapEntry[];
seriesCountByLabelName: TopHeapEntry[];
seriesCountByFocusLabelValue: TopHeapEntry[];
@ -14,6 +15,8 @@ export interface TSDBStatus {
export interface TopHeapEntry {
name: string;
value: number;
diff: number;
valuePrev: number;
}
interface QueryUpdaterArgs {

View file

@ -0,0 +1,26 @@
@use "src/styles/variables" as *;
.vm-dynamic-number {
font-size: $font-size-small;
color: $color-text-disabled;
&_positive {
color: $color-success;
}
&_negative {
color: $color-error;
}
&_down {
&:before {
content: "";
}
}
&_up {
&:before {
content: "";
}
}
}

View file

@ -10,6 +10,7 @@
@forward "./components/sectionheader";
@forward "./components/table";
@forward "./components/link";
@forward "./components/dynamic-number";
:root {
/* base palette */

View file

@ -25,6 +25,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: introduce `-http.maxConcurrentRequests` command-line flag to protect VM components from resource exhaustion during unexpected spikes of HTTP requests. By default, the new flag's value is set to 0 which means no limits are applied.
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add support for the different time formats for `--vm-native-filter-time-start` and `--vm-native-filter-time-end` flags if the native binary protocol is used for migration. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4091).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): integrate WITH template playground. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3811).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add a comparison of data from the previous day with data from the current day to the `Cardinality Explorer`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3967).
* FEATURE: [vmauth](https://docs.victoriametrics.com/vmauth.html): add ability to filter incoming requests by IP. See [these docs](https://docs.victoriametrics.com/vmauth.html#ip-filters) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3491).
* BUGFIX: reduce the probability of sudden increase in the number of small parts on systems with small number of CPU cores.