mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-11 15:34:56 +00:00
vmui: improve table view (#3377)
* vmui: add compact table view (#3365) * feat: add compact table view * fix: add overflow table * fix: change table styles * vmui: compact table view * Update docs/CHANGELOG.md Co-authored-by: Michal Kralik <michal.kralik@percona.com> Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
399ed9a3b9
commit
eb772aa50e
9 changed files with 208 additions and 126 deletions
|
@ -1,4 +1,4 @@
|
|||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import { InstantMetricResult } from "../../../api/types";
|
||||
import { InstantDataSeries } from "../../../types";
|
||||
import { useSortedCategories } from "../../../hooks/useSortedCategories";
|
||||
|
@ -8,6 +8,10 @@ import { ArrowDropDownIcon, CopyIcon } from "../../Main/Icons";
|
|||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { useSnack } from "../../../contexts/Snackbar";
|
||||
import { getNameForMetric } from "../../../utils/metric";
|
||||
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||
import "./style.scss";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data: InstantMetricResult[];
|
||||
|
@ -17,12 +21,21 @@ export interface GraphViewProps {
|
|||
const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
|
||||
const sortedColumns = useSortedCategories(data, displayColumns);
|
||||
const { tableCompact } = useCustomPanelState();
|
||||
const windowSize = useResize(document.body);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const [tableTop, setTableTop] = useState(0);
|
||||
const [headTop, setHeadTop] = useState(0);
|
||||
|
||||
const [orderBy, setOrderBy] = useState("");
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
const getCopyValue = (metric: {[p: string]: string}) => {
|
||||
const sortedColumns = (tableCompact
|
||||
? useSortedCategories([{ group: 0, metric: { "Data": "Data" } }], ["Data"])
|
||||
: useSortedCategories(data, displayColumns)
|
||||
);
|
||||
|
||||
const getCopyValue = (metric: { [p: string]: string }) => {
|
||||
const { __name__, ...fields } = metric;
|
||||
if (!__name__ && !Object.keys(fields).length) return "";
|
||||
return `${__name__} ${JSON.stringify(fields)}`;
|
||||
|
@ -30,20 +43,23 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
|
|||
|
||||
const rows: InstantDataSeries[] = useMemo(() => {
|
||||
const rows = data?.map(d => ({
|
||||
metadata: sortedColumns.map(c => d.metric[c.key] || "-"),
|
||||
metadata: sortedColumns.map(c => (tableCompact
|
||||
? getNameForMetric(d, undefined, "=", true)
|
||||
: (d.metric[c.key] || "-")
|
||||
)),
|
||||
value: d.value ? d.value[1] : "-",
|
||||
copyValue: getCopyValue(d.metric)
|
||||
}));
|
||||
const orderByValue = orderBy === "Value";
|
||||
const rowIndex = sortedColumns.findIndex(c => c.key === orderBy);
|
||||
if (!orderByValue && rowIndex === -1) return rows;
|
||||
return rows.sort((a,b) => {
|
||||
return rows.sort((a, b) => {
|
||||
const n1 = orderByValue ? Number(a.value) : a.metadata[rowIndex];
|
||||
const n2 = orderByValue ? Number(b.value) : b.metadata[rowIndex];
|
||||
const asc = orderDir === "asc" ? n1 < n2 : n1 > n2;
|
||||
return asc ? -1 : 1;
|
||||
});
|
||||
}, [sortedColumns, data, orderBy, orderDir]);
|
||||
}, [sortedColumns, data, orderBy, orderDir, tableCompact]);
|
||||
|
||||
const hasCopyValue = useMemo(() => rows.some(r => r.copyValue), [rows]);
|
||||
|
||||
|
@ -65,93 +81,121 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
|
|||
copyHandler(copyValue);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!tableRef.current) return;
|
||||
const { top } = tableRef.current.getBoundingClientRect();
|
||||
setHeadTop(top < 0 ? window.scrollY - tableTop : 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [tableRef, tableTop, windowSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableRef.current) return;
|
||||
const { top } = tableRef.current.getBoundingClientRect();
|
||||
setTableTop(top + window.scrollY);
|
||||
}, [tableRef, windowSize]);
|
||||
|
||||
if (!rows.length) return <Alert variant="warning">No data to show</Alert>;
|
||||
|
||||
return (
|
||||
<table className="vm-table">
|
||||
<thead className="vm-table-header">
|
||||
<tr className="vm-table__row vm-table__row_header">
|
||||
{sortedColumns.map((col, index) => (
|
||||
<div className="vm-table-view">
|
||||
<table
|
||||
className="vm-table"
|
||||
ref={tableRef}
|
||||
>
|
||||
<thead className="vm-table-header">
|
||||
<tr
|
||||
className="vm-table__row vm-table__row_header"
|
||||
style={{ transform: `translateY(${headTop}px)` }}
|
||||
>
|
||||
{sortedColumns.map((col, index) => (
|
||||
<td
|
||||
className="vm-table-cell vm-table-cell_header vm-table-cell_sort"
|
||||
key={index}
|
||||
onClick={createSortHandler(col.key)}
|
||||
>
|
||||
<div className="vm-table-cell__content">
|
||||
{col.key}
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-table__sort-icon": true,
|
||||
"vm-table__sort-icon_active": orderBy === col.key,
|
||||
"vm-table__sort-icon_desc": orderDir === "desc" && orderBy === col.key
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
<td
|
||||
className="vm-table-cell vm-table-cell_header vm-table-cell_sort"
|
||||
key={index}
|
||||
onClick={createSortHandler(col.key)}
|
||||
className="vm-table-cell vm-table-cell_header vm-table-cell_right vm-table-cell_sort"
|
||||
onClick={createSortHandler("Value")}
|
||||
>
|
||||
<div className="vm-table-cell__content">
|
||||
{col.key}
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-table__sort-icon": true,
|
||||
"vm-table__sort-icon_active": orderBy === col.key,
|
||||
"vm-table__sort-icon_desc": orderDir === "desc" && orderBy === col.key
|
||||
"vm-table__sort-icon_active": orderBy === "Value",
|
||||
"vm-table__sort-icon_desc": orderDir === "desc"
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
Value
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
<td
|
||||
className="vm-table-cell vm-table-cell_header vm-table-cell_right vm-table-cell_sort"
|
||||
onClick={createSortHandler("Value")}
|
||||
>
|
||||
<div className="vm-table-cell__content">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-table__sort-icon": true,
|
||||
"vm-table__sort-icon_active": orderBy === "Value",
|
||||
"vm-table__sort-icon_desc": orderDir === "desc"
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
Value
|
||||
</div>
|
||||
</td>
|
||||
{hasCopyValue && <td className="vm-table-cell vm-table-cell_header"/>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="vm-table-body">
|
||||
{rows.map((row, index) => (
|
||||
<tr
|
||||
className="vm-table__row"
|
||||
key={index}
|
||||
>
|
||||
{row.metadata.map((rowMeta, index2) => (
|
||||
<td
|
||||
className={classNames({
|
||||
"vm-table-cell vm-table-cell_no-wrap": true,
|
||||
"vm-table-cell_gray": rows[index - 1] && rows[index - 1].metadata[index2] === rowMeta
|
||||
})}
|
||||
key={index2}
|
||||
>
|
||||
{rowMeta}
|
||||
</td>
|
||||
))}
|
||||
<td className="vm-table-cell vm-table-cell_right">
|
||||
{row.value}
|
||||
</td>
|
||||
{hasCopyValue && (
|
||||
<td className="vm-table-cell vm-table-cell_right">
|
||||
{row.copyValue && (
|
||||
<div className="vm-table-cell__content">
|
||||
<Tooltip title="Copy row">
|
||||
<Button
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
startIcon={<CopyIcon/>}
|
||||
onClick={createCopyHandler(row.copyValue)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{hasCopyValue && <td className="vm-table-cell vm-table-cell_header"/>}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="vm-table-body">
|
||||
{rows.map((row, index) => (
|
||||
<tr
|
||||
className="vm-table__row"
|
||||
key={index}
|
||||
>
|
||||
{row.metadata.map((rowMeta, index2) => (
|
||||
<td
|
||||
className={classNames({
|
||||
"vm-table-cell vm-table-cell_no-wrap": true,
|
||||
"vm-table-cell_gray": rows[index - 1] && rows[index - 1].metadata[index2] === rowMeta
|
||||
})}
|
||||
key={index2}
|
||||
>
|
||||
{rowMeta}
|
||||
</td>
|
||||
))}
|
||||
<td className="vm-table-cell vm-table-cell_right">
|
||||
{row.value}
|
||||
</td>
|
||||
{hasCopyValue && (
|
||||
<td className="vm-table-cell vm-table-cell_right">
|
||||
{row.copyValue && (
|
||||
<div className="vm-table-cell__content">
|
||||
<Tooltip title="Copy row">
|
||||
<Button
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
startIcon={<CopyIcon/>}
|
||||
onClick={createCopyHandler(row.copyValue)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-table-view {
|
||||
margin-top: -$padding-medium;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
table {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
|
@ -2,13 +2,16 @@ import React, { FC, useEffect, useState, useRef, useMemo } from "preact/compat";
|
|||
import { useSortedCategories } from "../../../../hooks/useSortedCategories";
|
||||
import { InstantMetricResult } from "../../../../api/types";
|
||||
import Button from "../../../../components/Main/Button/Button";
|
||||
import { CloseIcon, SettingsIcon } from "../../../../components/Main/Icons";
|
||||
import { CloseIcon, RestartIcon, SettingsIcon } from "../../../../components/Main/Icons";
|
||||
import Popper from "../../../../components/Main/Popper/Popper";
|
||||
import "./style.scss";
|
||||
import Checkbox from "../../../../components/Main/Checkbox/Checkbox";
|
||||
import Tooltip from "../../../../components/Main/Tooltip/Tooltip";
|
||||
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
|
||||
import Switch from "../../../../components/Main/Switch/Switch";
|
||||
import { arrayEquals } from "../../../../utils/array";
|
||||
|
||||
const title = "Display columns";
|
||||
const title = "Table settings";
|
||||
|
||||
interface TableSettingsProps {
|
||||
data: InstantMetricResult[];
|
||||
|
@ -16,32 +19,33 @@ interface TableSettingsProps {
|
|||
onChange: (arr: string[]) => void
|
||||
}
|
||||
|
||||
const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns, onChange }) => {
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onChange }) => {
|
||||
|
||||
const { tableCompact } = useCustomPanelState();
|
||||
const customPanelDispatch = useCustomPanelDispatch();
|
||||
const columns = useSortedCategories(data);
|
||||
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
||||
const disabledButton = useMemo(() => !columns.length, [columns]);
|
||||
const [checkedColumns, setCheckedColumns] = useState(columns.map(col => col.key));
|
||||
|
||||
const handleChange = (key: string) => {
|
||||
setCheckedColumns(prev => checkedColumns.includes(key) ? prev.filter(col => col !== key) : [...prev, key]);
|
||||
onChange(defaultColumns.includes(key) ? defaultColumns.filter(col => col !== key) : [...defaultColumns, key]);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpenSettings(false);
|
||||
setCheckedColumns(defaultColumns || columns.map(col => col.key));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setOpenSettings(false);
|
||||
const value = columns.map(col => col.key);
|
||||
setCheckedColumns(value);
|
||||
onChange(value);
|
||||
const toggleTableCompact = () => {
|
||||
customPanelDispatch({ type: "TOGGLE_TABLE_COMPACT" });
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
const handleResetColumns = () => {
|
||||
setOpenSettings(false);
|
||||
onChange(checkedColumns);
|
||||
onChange(columns.map(col => col.key));
|
||||
};
|
||||
|
||||
const createHandlerChange = (key: string) => () => {
|
||||
|
@ -53,7 +57,9 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns, onChange
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCheckedColumns(columns.map(col => col.key));
|
||||
const values = columns.map(col => col.key);
|
||||
if (arrayEquals(values, defaultColumns)) return;
|
||||
onChange(values);
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
|
@ -86,36 +92,39 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns, onChange
|
|||
/>
|
||||
</div>
|
||||
<div className="vm-table-settings-popper-list">
|
||||
<Switch
|
||||
label={"Compact view"}
|
||||
value={tableCompact}
|
||||
onChange={toggleTableCompact}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-table-settings-popper-list">
|
||||
<div className="vm-table-settings-popper-list-header">
|
||||
<h3 className="vm-table-settings-popper-list-header__title">Display columns</h3>
|
||||
<Tooltip title="Reset to default">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="text"
|
||||
size="small"
|
||||
onClick={handleResetColumns}
|
||||
startIcon={<RestartIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{columns.map(col => (
|
||||
<div
|
||||
className="vm-table-settings-popper-list__item"
|
||||
key={col.key}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checkedColumns.includes(col.key)}
|
||||
checked={defaultColumns.includes(col.key)}
|
||||
onChange={createHandlerChange(col.key)}
|
||||
label={col.key}
|
||||
disabled={tableCompact}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="vm-table-settings-popper__footer">
|
||||
<Button
|
||||
color="error"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleApply}
|
||||
>
|
||||
apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
|
|
|
@ -2,27 +2,31 @@
|
|||
|
||||
.vm-table-settings-popper {
|
||||
display: grid;
|
||||
min-width: 250px;
|
||||
|
||||
&-list {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
padding: $padding-global $padding-global $padding-medium;
|
||||
padding: $padding-global;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: 1fr auto;
|
||||
min-height: 25px;
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
font-size: $font-size;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
min-width: 200px;
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 $padding-global $padding-global;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.vm-custom-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
height: 100%;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DisplayType, displayTypeTabs } from "../../pages/CustomPanel/DisplayTypeSwitch";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
import { SeriesLimits } from "../../types";
|
||||
import { DEFAULT_MAX_SERIES } from "../../constants/graph";
|
||||
|
||||
|
@ -9,6 +9,7 @@ export interface CustomPanelState {
|
|||
nocache: boolean;
|
||||
isTracingEnabled: boolean;
|
||||
seriesLimits: SeriesLimits
|
||||
tableCompact: boolean;
|
||||
}
|
||||
|
||||
export type CustomPanelAction =
|
||||
|
@ -16,6 +17,7 @@ export type CustomPanelAction =
|
|||
| { type: "SET_SERIES_LIMITS", payload: SeriesLimits }
|
||||
| { type: "TOGGLE_NO_CACHE"}
|
||||
| { type: "TOGGLE_QUERY_TRACING" }
|
||||
| { type: "TOGGLE_TABLE_COMPACT" }
|
||||
|
||||
const queryTab = getQueryStringValue("g0.tab", 0) as string;
|
||||
const displayType = displayTypeTabs.find(t => t.prometheusCode === +queryTab || t.value === queryTab);
|
||||
|
@ -25,7 +27,8 @@ export const initialCustomPanelState: CustomPanelState = {
|
|||
displayType: (displayType?.value || "chart") as DisplayType,
|
||||
nocache: false,
|
||||
isTracingEnabled: false,
|
||||
seriesLimits: limitsStorage ? JSON.parse(getFromStorage("SERIES_LIMITS") as string) : DEFAULT_MAX_SERIES
|
||||
seriesLimits: limitsStorage ? JSON.parse(getFromStorage("SERIES_LIMITS") as string) : DEFAULT_MAX_SERIES,
|
||||
tableCompact: getFromStorage("TABLE_COMPACT") as boolean || false,
|
||||
};
|
||||
|
||||
export function reducer(state: CustomPanelState, action: CustomPanelAction): CustomPanelState {
|
||||
|
@ -52,6 +55,12 @@ export function reducer(state: CustomPanelState, action: CustomPanelAction): Cus
|
|||
...state,
|
||||
nocache: !state.nocache
|
||||
};
|
||||
case "TOGGLE_TABLE_COMPACT":
|
||||
saveToStorage("TABLE_COMPACT", !state.tableCompact);
|
||||
return {
|
||||
...state,
|
||||
tableCompact: !state.tableCompact
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MetricBase } from "../api/types";
|
||||
|
||||
export const getNameForMetric = (result: MetricBase, alias?: string): string => {
|
||||
export const getNameForMetric = (result: MetricBase, alias?: string, connector = ": ", quoteValue = false): string => {
|
||||
const { __name__, ...freeFormFields } = result.metric;
|
||||
const name = alias || __name__ || "";
|
||||
|
||||
|
@ -8,5 +8,7 @@ export const getNameForMetric = (result: MetricBase, alias?: string): string =>
|
|||
return name || `Result ${result.group}`; // a bit better than just {} for case of aggregation functions
|
||||
}
|
||||
|
||||
return `${name} {${Object.entries(freeFormFields).map(e => `${e[0]}: ${e[1]}`).join(", ")}}`;
|
||||
return `${name} {${Object.entries(freeFormFields).map(e =>
|
||||
`${e[0]}${connector}${(quoteValue ? `"${e[1]}"` : e[1])}`
|
||||
).join(", ")}}`;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
|
|||
| "NO_CACHE"
|
||||
| "QUERY_TRACING"
|
||||
| "SERIES_LIMITS"
|
||||
| "TABLE_COMPACT"
|
||||
|
||||
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
|
||||
if (value) {
|
||||
|
|
|
@ -25,6 +25,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce JS bundle size from 200Kb to 100Kb. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3298).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to hide results of a particular query by clicking the `eye` icon. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3359).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add copy button to row on Table view. The button copies row in MetricQL format. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2815).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add compact table view. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3241).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to "stick" a tooltip on the chart by clicking on a data point. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3321) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3376)
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to set up series custom limits. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3297).
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add default alert list for vmalert's metrics. See [alerts-vmalert.yml](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmalert.yml).
|
||||
|
|
Loading…
Reference in a new issue