mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui/logs: optimize memory consumption
This commit is contained in:
parent
371e193279
commit
d14923171a
15 changed files with 375 additions and 216 deletions
|
@ -20,6 +20,7 @@ module.exports = {
|
||||||
"@typescript-eslint"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
|
||||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||||
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
|
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
|
||||||
"react/jsx-first-prop-new-line": [1, "multiline"],
|
"react/jsx-first-prop-new-line": [1, "multiline"],
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ArrowDownIcon } from "../Icons";
|
||||||
|
import { useMemo } from "preact/compat";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalItems: number;
|
||||||
|
itemsPerPage: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
maxVisiblePages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: React.FC<PaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalItems,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
maxVisiblePages = 10
|
||||||
|
}) => {
|
||||||
|
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
if (page < 1 || page > totalPages) return;
|
||||||
|
onPageChange(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = useMemo(() => {
|
||||||
|
const pages = [];
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||||
|
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||||
|
|
||||||
|
if (startPage > 1) {
|
||||||
|
pages.push(1);
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pages.push("...");
|
||||||
|
}
|
||||||
|
pages.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}, [totalPages, currentPage, maxVisiblePages]);
|
||||||
|
|
||||||
|
const handleClickNav = (stepPage: number) => () => {
|
||||||
|
handlePageChange(currentPage + stepPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickPage = (page: number | string) => () => {
|
||||||
|
if (typeof page === "number") {
|
||||||
|
handlePageChange(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pages.length <= 1) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="vm-pagination">
|
||||||
|
<button
|
||||||
|
className="vm-pagination__page vm-pagination__arrow vm-pagination__arrow_prev"
|
||||||
|
onClick={handleClickNav(-1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon/>
|
||||||
|
</button>
|
||||||
|
{pages.map((page, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={handleClickPage(page)}
|
||||||
|
className={classNames({
|
||||||
|
"vm-pagination__page": true,
|
||||||
|
"vm-pagination__page_active": currentPage === page,
|
||||||
|
"vm-pagination__page_disabled": page === "..."
|
||||||
|
})}
|
||||||
|
disabled={page === "..."}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="vm-pagination__page vm-pagination__arrow vm-pagination__arrow_next"
|
||||||
|
onClick={handleClickNav(1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ArrowDownIcon/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
|
@ -1,52 +0,0 @@
|
||||||
import React, { FC } from "preact/compat";
|
|
||||||
import Button from "../../Button/Button";
|
|
||||||
import { ArrowDownIcon } from "../../Icons";
|
|
||||||
import "./style.scss";
|
|
||||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface PaginationControlProps {
|
|
||||||
page: number;
|
|
||||||
length: number;
|
|
||||||
limit: number;
|
|
||||||
onChange: (page: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaginationControl: FC<PaginationControlProps> = ({ page, length, limit, onChange }) => {
|
|
||||||
const { isMobile } = useDeviceDetect();
|
|
||||||
|
|
||||||
const handleChangePage = (step: number) => () => {
|
|
||||||
onChange(+page + step);
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames({
|
|
||||||
"vm-pagination": true,
|
|
||||||
"vm-pagination_mobile": isMobile
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{page > 1 && (
|
|
||||||
<Button
|
|
||||||
variant={"text"}
|
|
||||||
onClick={handleChangePage(-1)}
|
|
||||||
startIcon={<div className="vm-pagination__icon vm-pagination__icon_prev"><ArrowDownIcon/></div>}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{length >= limit && (
|
|
||||||
<Button
|
|
||||||
variant={"text"}
|
|
||||||
onClick={handleChangePage(1)}
|
|
||||||
endIcon={<div className="vm-pagination__icon vm-pagination__icon_next"><ArrowDownIcon/></div>}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PaginationControl;
|
|
|
@ -1,24 +0,0 @@
|
||||||
@use "src/styles/variables" as *;
|
|
||||||
|
|
||||||
.vm-pagination {
|
|
||||||
position: sticky;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: $padding-small;
|
|
||||||
padding: $padding-global 0 0;
|
|
||||||
|
|
||||||
&_mobile {
|
|
||||||
padding: $padding-global 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
&_prev {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&_next {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
@use "../../../styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-pagination {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: $padding-small;
|
||||||
|
padding: $padding-global 0;
|
||||||
|
font-size: $font-size;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
padding: $padding-global 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
padding: 0 $padding-small;
|
||||||
|
border-radius: $border-radius-small;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&_active {
|
||||||
|
background-color: $color-primary;
|
||||||
|
color: $color-primary-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_disabled {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-hover-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__arrow {
|
||||||
|
|
||||||
|
svg {
|
||||||
|
max-width: $font-size;
|
||||||
|
max-height: $font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_prev {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_next {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,8 +32,7 @@ const Table = <T extends object>({ rows, columns, defaultOrderBy, defaultOrderDi
|
||||||
const sortedList = useMemo(() => {
|
const sortedList = useMemo(() => {
|
||||||
const { startIndex, endIndex } = paginationOffset;
|
const { startIndex, endIndex } = paginationOffset;
|
||||||
return stableSort(rows as [], getComparator(orderDir, orderBy)).slice(startIndex, endIndex);
|
return stableSort(rows as [], getComparator(orderDir, orderBy)).slice(startIndex, endIndex);
|
||||||
},
|
}, [rows, orderBy, orderDir, paginationOffset]);
|
||||||
[rows, orderBy, orderDir, paginationOffset]);
|
|
||||||
|
|
||||||
const createSortHandler = (key: keyof T) => () => {
|
const createSortHandler = (key: keyof T) => () => {
|
||||||
setOrderDir((prev) => prev === "asc" && orderBy === key ? "desc" : "asc");
|
setOrderDir((prev) => prev === "asc" && orderBy === key ? "desc" : "asc");
|
||||||
|
|
|
@ -12,17 +12,7 @@ export interface JsonViewProps {
|
||||||
const JsonView: FC<JsonViewProps> = ({ data }) => {
|
const JsonView: FC<JsonViewProps> = ({ data }) => {
|
||||||
const copyToClipboard = useCopyToClipboard();
|
const copyToClipboard = useCopyToClipboard();
|
||||||
|
|
||||||
const formattedJson = useMemo(() => {
|
const formattedJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
||||||
const space = " ";
|
|
||||||
const values = data.map(item => {
|
|
||||||
if (Object.keys(item).length === 1) {
|
|
||||||
return JSON.stringify(item);
|
|
||||||
} else {
|
|
||||||
return JSON.stringify(item, null, space.length);
|
|
||||||
}
|
|
||||||
}).join(",\n").replace(/^/gm, `${space}`);
|
|
||||||
return `[\n${values}\n]`;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const handlerCopy = async () => {
|
const handlerCopy = async () => {
|
||||||
await copyToClipboard(formattedJson, "Formatted JSON has been copied");
|
await copyToClipboard(formattedJson, "Formatted JSON has been copied");
|
||||||
|
|
|
@ -54,7 +54,7 @@ const ExploreLogs: FC = () => {
|
||||||
fetchLogs(newPeriod).then((isSuccess) => {
|
fetchLogs(newPeriod).then((isSuccess) => {
|
||||||
isSuccess && !hideChart && fetchLogHits(newPeriod);
|
isSuccess && !hideChart && fetchLogHits(newPeriod);
|
||||||
}).catch(e => e);
|
}).catch(e => e);
|
||||||
setSearchParamsFromKeys( {
|
setSearchParamsFromKeys({
|
||||||
query,
|
query,
|
||||||
"g0.range_input": duration,
|
"g0.range_input": duration,
|
||||||
"g0.end_input": newPeriod.date,
|
"g0.end_input": newPeriod.date,
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
import React, { FC, useState, useMemo, useRef } from "preact/compat";
|
import React, { FC, useState, useMemo, useRef } from "preact/compat";
|
||||||
import JsonView from "../../../components/Views/JsonView/JsonView";
|
|
||||||
import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
|
import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
|
||||||
import Tabs from "../../../components/Main/Tabs/Tabs";
|
import Tabs from "../../../components/Main/Tabs/Tabs";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import { Logs } from "../../../api/types";
|
import { Logs } from "../../../api/types";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
|
||||||
import useStateSearchParams from "../../../hooks/useStateSearchParams";
|
import useStateSearchParams from "../../../hooks/useStateSearchParams";
|
||||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
||||||
import TableSettings from "../../../components/Table/TableSettings/TableSettings";
|
import TableSettings from "../../../components/Table/TableSettings/TableSettings";
|
||||||
import useBoolean from "../../../hooks/useBoolean";
|
import useBoolean from "../../../hooks/useBoolean";
|
||||||
import TableLogs from "./TableLogs";
|
import TableLogs from "./TableLogs";
|
||||||
import GroupLogs from "../GroupLogs/GroupLogs";
|
import GroupLogs from "../GroupLogs/GroupLogs";
|
||||||
import { DATE_TIME_FORMAT } from "../../../constants/date";
|
import JsonView from "../../../components/Views/JsonView/JsonView";
|
||||||
import { marked } from "marked";
|
|
||||||
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
|
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
|
||||||
|
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
|
||||||
|
|
||||||
|
const MemoizedTableLogs = React.memo(TableLogs);
|
||||||
|
const MemoizedGroupLogs = React.memo(GroupLogs);
|
||||||
|
const MemoizedJsonView = React.memo(JsonView);
|
||||||
|
|
||||||
export interface ExploreLogBodyProps {
|
export interface ExploreLogBodyProps {
|
||||||
data: Logs[];
|
data: Logs[];
|
||||||
|
@ -37,38 +38,35 @@ const tabs = [
|
||||||
|
|
||||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
const { timezone } = useTimeState();
|
|
||||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||||
const groupSettingsRef = useRef<HTMLDivElement>(null);
|
const groupSettingsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
|
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
|
||||||
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
|
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(1000, "rows_per_page");
|
||||||
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
|
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
|
||||||
|
|
||||||
const logs = useMemo(() => data.map((item) => ({
|
|
||||||
...item,
|
|
||||||
_vmui_time: item._time ? dayjs(item._time).tz().format(`${DATE_TIME_FORMAT}.SSS`) : "",
|
|
||||||
_vmui_data: JSON.stringify(item, null, 2),
|
|
||||||
_vmui_markdown: item._msg ? marked(item._msg.replace(/```/g, "\n```\n")) as string : ""
|
|
||||||
})) as Logs[], [data, timezone]);
|
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
if (!logs?.length) return [];
|
if (!data?.length) return [];
|
||||||
const hideColumns = ["_vmui_data", "_vmui_time", "_vmui_markdown"];
|
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
for (const item of logs) {
|
for (const item of data) {
|
||||||
for (const key in item) {
|
for (const key in item) {
|
||||||
keys.add(key);
|
keys.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Array.from(keys).filter((col) => !hideColumns.includes(col));
|
return Array.from(keys);
|
||||||
}, [logs]);
|
}, [data]);
|
||||||
|
|
||||||
const handleChangeTab = (view: string) => {
|
const handleChangeTab = (view: string) => {
|
||||||
setActiveTab(view as DisplayType);
|
setActiveTab(view as DisplayType);
|
||||||
setSearchParamsFromKeys({ view });
|
setSearchParamsFromKeys({ view });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetRowsPerPage = (limit: number) => {
|
||||||
|
setRowsPerPage(limit);
|
||||||
|
setSearchParamsFromKeys({ rows_per_page: limit });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -97,6 +95,10 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||||
</div>
|
</div>
|
||||||
{activeTab === DisplayType.table && (
|
{activeTab === DisplayType.table && (
|
||||||
<div className="vm-explore-logs-body-header__settings">
|
<div className="vm-explore-logs-body-header__settings">
|
||||||
|
<SelectLimit
|
||||||
|
limit={rowsPerPage}
|
||||||
|
onChange={handleSetRowsPerPage}
|
||||||
|
/>
|
||||||
<TableSettings
|
<TableSettings
|
||||||
columns={columns}
|
columns={columns}
|
||||||
selectedColumns={displayColumns}
|
selectedColumns={displayColumns}
|
||||||
|
@ -124,22 +126,22 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||||
{!!data.length && (
|
{!!data.length && (
|
||||||
<>
|
<>
|
||||||
{activeTab === DisplayType.table && (
|
{activeTab === DisplayType.table && (
|
||||||
<TableLogs
|
<MemoizedTableLogs
|
||||||
logs={logs}
|
logs={data}
|
||||||
displayColumns={displayColumns}
|
displayColumns={displayColumns}
|
||||||
tableCompact={tableCompact}
|
tableCompact={tableCompact}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
rowsPerPage={Number(rowsPerPage)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === DisplayType.group && (
|
{activeTab === DisplayType.group && (
|
||||||
<GroupLogs
|
<MemoizedGroupLogs
|
||||||
logs={logs}
|
logs={data}
|
||||||
columns={columns}
|
|
||||||
settingsRef={groupSettingsRef}
|
settingsRef={groupSettingsRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === DisplayType.json && (
|
{activeTab === DisplayType.json && (
|
||||||
<JsonView data={data}/>
|
<MemoizedJsonView data={data}/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,58 +1,94 @@
|
||||||
import React, { FC, useMemo } from "preact/compat";
|
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import Table from "../../../components/Table/Table";
|
import Table from "../../../components/Table/Table";
|
||||||
import { Logs } from "../../../api/types";
|
import { Logs } from "../../../api/types";
|
||||||
|
import Pagination from "../../../components/Main/Pagination/Pagination";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
interface TableLogsProps {
|
interface TableLogsProps {
|
||||||
logs: Logs[];
|
logs: Logs[];
|
||||||
displayColumns: string[];
|
displayColumns: string[];
|
||||||
tableCompact: boolean;
|
tableCompact: boolean;
|
||||||
columns: string[];
|
columns: string[];
|
||||||
|
rowsPerPage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, columns }) => {
|
const getColumnClass = (key: string) => {
|
||||||
const getColumnClass = (key: string) => {
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "_time":
|
case "_time":
|
||||||
return "vm-table-cell_logs-time";
|
return "vm-table-cell_logs-time";
|
||||||
case "_vmui_data":
|
|
||||||
return "vm-table-cell_logs vm-table-cell_pre";
|
|
||||||
default:
|
default:
|
||||||
return "vm-table-cell_logs";
|
return "vm-table-cell_logs";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableColumns = useMemo(() => {
|
const compactColumns = [{
|
||||||
if (tableCompact) {
|
|
||||||
return [{
|
|
||||||
key: "_vmui_data",
|
key: "_vmui_data",
|
||||||
title: "Data",
|
title: "Data",
|
||||||
className: getColumnClass("_vmui_data")
|
className: "vm-table-cell_logs vm-table-cell_pre"
|
||||||
}];
|
}];
|
||||||
}
|
|
||||||
|
const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, columns, rowsPerPage }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
return logs.map((log) => {
|
||||||
|
const _vmui_data = JSON.stringify(log, null, 2);
|
||||||
|
return { ...log, _vmui_data };
|
||||||
|
}) as Logs[];
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
|
const tableColumns = useMemo(() => {
|
||||||
return columns.map((key) => ({
|
return columns.map((key) => ({
|
||||||
key: key as keyof Logs,
|
key: key as keyof Logs,
|
||||||
title: key,
|
title: key,
|
||||||
className: getColumnClass(key),
|
className: getColumnClass(key),
|
||||||
}));
|
}));
|
||||||
}, [tableCompact, columns]);
|
}, [columns]);
|
||||||
|
|
||||||
|
|
||||||
const filteredColumns = useMemo(() => {
|
const filteredColumns = useMemo(() => {
|
||||||
if (tableCompact) return tableColumns;
|
if (tableCompact) return compactColumns;
|
||||||
if (!displayColumns?.length) return [];
|
if (!displayColumns?.length) return [];
|
||||||
return tableColumns.filter(c => displayColumns.includes(c.key as string));
|
return tableColumns.filter(c => displayColumns.includes(c.key as string));
|
||||||
}, [tableColumns, displayColumns, tableCompact]);
|
}, [tableColumns, displayColumns, tableCompact]);
|
||||||
|
|
||||||
|
const paginationOffset = useMemo(() => {
|
||||||
|
const startIndex = (page - 1) * rowsPerPage;
|
||||||
|
const endIndex = startIndex + rowsPerPage;
|
||||||
|
return { startIndex, endIndex };
|
||||||
|
}, [page, rowsPerPage]);
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
setPage(newPage);
|
||||||
|
if (containerRef.current) {
|
||||||
|
const y = containerRef.current.getBoundingClientRect().top + window.scrollY - 50;
|
||||||
|
window.scrollTo({ top: y });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [logs, rowsPerPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div ref={containerRef}>
|
||||||
<Table
|
<Table
|
||||||
rows={logs}
|
rows={rows}
|
||||||
columns={filteredColumns}
|
columns={filteredColumns}
|
||||||
defaultOrderBy={"_time"}
|
defaultOrderBy={"_time"}
|
||||||
defaultOrderDir={"desc"}
|
defaultOrderDir={"desc"}
|
||||||
copyToClipboard={"_vmui_data"}
|
copyToClipboard={"_vmui_data"}
|
||||||
paginationOffset={{ startIndex: 0, endIndex: Infinity }}
|
paginationOffset={paginationOffset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalItems={rows.length}
|
||||||
|
itemsPerPage={rowsPerPage}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,13 +20,12 @@ import { getStreamPairs } from "../../../utils/logs";
|
||||||
|
|
||||||
const WITHOUT_GROUPING = "No Grouping";
|
const WITHOUT_GROUPING = "No Grouping";
|
||||||
|
|
||||||
interface TableLogsProps {
|
interface Props {
|
||||||
logs: Logs[];
|
logs: Logs[];
|
||||||
columns: string[];
|
|
||||||
settingsRef: React.RefObject<HTMLElement>;
|
settingsRef: React.RefObject<HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const copyToClipboard = useCopyToClipboard();
|
const copyToClipboard = useCopyToClipboard();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
@ -46,19 +45,21 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||||
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
|
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
|
||||||
|
|
||||||
const logsKeys = useMemo(() => {
|
const logsKeys = useMemo(() => {
|
||||||
const excludeKeys = ["_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
const excludeKeys = ["_msg", "_time"];
|
||||||
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
|
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
|
||||||
const keys = [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
|
return [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
|
||||||
|
}, [logs]);
|
||||||
|
|
||||||
if (!searchKey) return keys;
|
const filteredLogsKeys = useMemo(() => {
|
||||||
|
if (!searchKey) return logsKeys;
|
||||||
try {
|
try {
|
||||||
const regexp = new RegExp(searchKey, "i");
|
const regexp = new RegExp(searchKey, "i");
|
||||||
const found = keys.filter((item) => regexp.test(item));
|
return logsKeys.filter(item => regexp.test(item))
|
||||||
return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
|
.sort((a, b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [logs, searchKey]);
|
}, [logsKeys, searchKey]);
|
||||||
|
|
||||||
const groupData = useMemo(() => {
|
const groupData = useMemo(() => {
|
||||||
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
|
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
|
||||||
|
@ -90,16 +91,15 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||||
|
|
||||||
const handleToggleExpandAll = useCallback(() => {
|
const handleToggleExpandAll = useCallback(() => {
|
||||||
setExpandGroups(new Array(groupData.length).fill(!expandAll));
|
setExpandGroups(new Array(groupData.length).fill(!expandAll));
|
||||||
}, [expandAll]);
|
}, [expandAll, groupData.length]);
|
||||||
|
|
||||||
const handleChangeExpand = (i: number) => (value: boolean) => {
|
const handleChangeExpand = useCallback((i: number) => (value: boolean) => {
|
||||||
setExpandGroups((prev) => {
|
setExpandGroups((prev) => {
|
||||||
const newExpandGroups = [...prev];
|
const newExpandGroups = [...prev];
|
||||||
newExpandGroups[i] = value;
|
newExpandGroups[i] = value;
|
||||||
return newExpandGroups;
|
return newExpandGroups;
|
||||||
});
|
});
|
||||||
|
}, []);
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (copied === null) return;
|
if (copied === null) return;
|
||||||
|
@ -166,7 +166,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||||
<Tooltip title={expandAll ? "Collapse All" : "Expand All"}>
|
<Tooltip title={expandAll ? "Collapse All" : "Expand All"}>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/> }
|
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/>}
|
||||||
onClick={handleToggleExpandAll}
|
onClick={handleToggleExpandAll}
|
||||||
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
|
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
|
||||||
/>
|
/>
|
||||||
|
@ -175,7 +175,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||||
<div ref={optionsButtonRef}>
|
<div ref={optionsButtonRef}>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
startIcon={<StorageIcon/> }
|
startIcon={<StorageIcon/>}
|
||||||
onClick={toggleOpenOptions}
|
onClick={toggleOpenOptions}
|
||||||
ariaLabel={"Group by"}
|
ariaLabel={"Group by"}
|
||||||
/>
|
/>
|
||||||
|
@ -197,7 +197,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||||
type="search"
|
type="search"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{logsKeys.map(id => (
|
{filteredLogsKeys.map(id => (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-list-item": true,
|
"vm-list-item": true,
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
|
||||||
|
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||||
|
import Button from "../../../components/Main/Button/Button";
|
||||||
|
import { CopyIcon } from "../../../components/Main/Icons";
|
||||||
|
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
|
||||||
|
const copyToClipboard = useCopyToClipboard();
|
||||||
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(async () => {
|
||||||
|
if (copied) return;
|
||||||
|
try {
|
||||||
|
await copyToClipboard(`${field}: "${value}"`);
|
||||||
|
setCopied(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}, [copied, copyToClipboard]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (copied === null) return;
|
||||||
|
const timeout = setTimeout(() => setCopied(false), 2000);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="vm-group-logs-row-fields-item">
|
||||||
|
<td className="vm-group-logs-row-fields-item-controls">
|
||||||
|
<div className="vm-group-logs-row-fields-item-controls__wrapper">
|
||||||
|
<Tooltip title={copied ? "Copied" : "Copy to clipboard"}>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="gray"
|
||||||
|
size="small"
|
||||||
|
startIcon={<CopyIcon/>}
|
||||||
|
onClick={handleCopy}
|
||||||
|
ariaLabel="copy to clipboard"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="vm-group-logs-row-fields-item__key">{field}</td>
|
||||||
|
<td className="vm-group-logs-row-fields-item__value">{value}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(GroupLogsFieldRow);
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
import React, { FC, memo, useMemo } from "preact/compat";
|
||||||
import { Logs } from "../../../api/types";
|
import { Logs } from "../../../api/types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import useBoolean from "../../../hooks/useBoolean";
|
import useBoolean from "../../../hooks/useBoolean";
|
||||||
import Button from "../../../components/Main/Button/Button";
|
import { ArrowDownIcon } from "../../../components/Main/Icons";
|
||||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
|
||||||
import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons";
|
|
||||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
|
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { DATE_TIME_FORMAT } from "../../../constants/date";
|
||||||
|
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||||
|
import GroupLogsFieldRow from "./GroupLogsFieldRow";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
log: Logs;
|
log: Logs;
|
||||||
|
@ -20,40 +22,31 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||||
} = useBoolean(false);
|
} = useBoolean(false);
|
||||||
|
|
||||||
const { markdownParsing } = useLogsState();
|
const { markdownParsing } = useLogsState();
|
||||||
|
const { timezone } = useTimeState();
|
||||||
|
|
||||||
const excludeKeys = ["_msg", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
const formattedTime = useMemo(() => {
|
||||||
const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
|
if (!log._time) return "";
|
||||||
|
return dayjs(log._time).tz().format(`${DATE_TIME_FORMAT}.SSS`);
|
||||||
|
}, [log._time, timezone]);
|
||||||
|
|
||||||
|
const formattedMarkdown = useMemo(() => {
|
||||||
|
if (!markdownParsing || !log._msg) return "";
|
||||||
|
return marked(log._msg.replace(/```/g, "\n```\n")) as string;
|
||||||
|
}, [log._msg, markdownParsing]);
|
||||||
|
|
||||||
|
const fields = useMemo(() => Object.entries(log).filter(([key]) => key !== "_msg"), [log]);
|
||||||
const hasFields = fields.length > 0;
|
const hasFields = fields.length > 0;
|
||||||
|
|
||||||
const displayMessage = useMemo(() => {
|
const displayMessage = useMemo(() => {
|
||||||
if (log._msg) return log._msg;
|
if (log._msg) return log._msg;
|
||||||
if (!hasFields) return;
|
if (!hasFields) return;
|
||||||
const dataObject = fields.reduce<{[key: string]: string}>((obj, [key, value]) => {
|
const dataObject = fields.reduce<{ [key: string]: string }>((obj, [key, value]) => {
|
||||||
obj[key] = value;
|
obj[key] = value;
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
return JSON.stringify(dataObject);
|
return JSON.stringify(dataObject);
|
||||||
}, [log, fields, hasFields]);
|
}, [log, fields, hasFields]);
|
||||||
|
|
||||||
const copyToClipboard = useCopyToClipboard();
|
|
||||||
const [copied, setCopied] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const createCopyHandler = (copyValue: string, rowIndex: number) => async () => {
|
|
||||||
if (copied === rowIndex) return;
|
|
||||||
try {
|
|
||||||
await copyToClipboard(copyValue);
|
|
||||||
setCopied(rowIndex);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (copied === null) return;
|
|
||||||
const timeout = setTimeout(() => setCopied(null), 2000);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [copied]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-group-logs-row">
|
<div className="vm-group-logs-row">
|
||||||
<div
|
<div
|
||||||
|
@ -74,10 +67,10 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-group-logs-row-content__time": true,
|
"vm-group-logs-row-content__time": true,
|
||||||
"vm-group-logs-row-content__time_missing": !log._vmui_time
|
"vm-group-logs-row-content__time_missing": !formattedTime
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{log._vmui_time || "timestamp missing"}
|
{formattedTime || "timestamp missing"}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -85,7 +78,7 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||||
"vm-group-logs-row-content__msg_empty-msg": !log._msg,
|
"vm-group-logs-row-content__msg_empty-msg": !log._msg,
|
||||||
"vm-group-logs-row-content__msg_missing": !displayMessage
|
"vm-group-logs-row-content__msg_missing": !displayMessage
|
||||||
})}
|
})}
|
||||||
dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
|
dangerouslySetInnerHTML={(markdownParsing && formattedMarkdown) ? { __html: formattedMarkdown } : undefined}
|
||||||
>
|
>
|
||||||
{displayMessage || "-"}
|
{displayMessage || "-"}
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,28 +87,12 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||||
<div className="vm-group-logs-row-fields">
|
<div className="vm-group-logs-row-fields">
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{fields.map(([key, value], i) => (
|
{fields.map(([key, value]) => (
|
||||||
<tr
|
<GroupLogsFieldRow
|
||||||
key={key}
|
key={key}
|
||||||
className="vm-group-logs-row-fields-item"
|
field={key}
|
||||||
>
|
value={value}
|
||||||
<td className="vm-group-logs-row-fields-item-controls">
|
|
||||||
<div className="vm-group-logs-row-fields-item-controls__wrapper">
|
|
||||||
<Tooltip title={copied === i ? "Copied" : "Copy to clipboard"}>
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
color="gray"
|
|
||||||
size="small"
|
|
||||||
startIcon={<CopyIcon/>}
|
|
||||||
onClick={createCopyHandler(`${key}: "${value}"`, i)}
|
|
||||||
ariaLabel="copy to clipboard"
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="vm-group-logs-row-fields-item__key">{key}</td>
|
|
||||||
<td className="vm-group-logs-row-fields-item__value">{value}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -125,4 +102,4 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupLogsItem;
|
export default memo(GroupLogsItem);
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [logs, setLogs] = useState<Logs[]>([]);
|
const [logs, setLogs] = useState<Logs[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<{[key: number]: boolean;}>([]);
|
const [isLoading, setIsLoading] = useState<{ [key: number]: boolean }>({});
|
||||||
const [error, setError] = useState<ErrorTypes | string>();
|
const [error, setError] = useState<ErrorTypes | string>();
|
||||||
const abortControllerRef = useRef(new AbortController());
|
const abortControllerRef = useRef(new AbortController());
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(line);
|
return JSON.parse(line);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error("Failed to parse line to JSON", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -56,23 +57,25 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
setError(text);
|
setError(text);
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
setIsLoading(prev => ({ ...prev, [id]: false }));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = text.split("\n").filter(line => line).slice(0, limit);
|
const data = text.split("\n", limit).map(parseLineToJSON).filter(line => line) as Logs[];
|
||||||
const data = lines.map(parseLineToJSON).filter(line => line) as Logs[];
|
|
||||||
setLogs(data);
|
setLogs(data);
|
||||||
setIsLoading(prev => ({ ...prev, [id]: false }));
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIsLoading(prev => ({ ...prev, [id]: false }));
|
|
||||||
if (e instanceof Error && e.name !== "AbortError") {
|
if (e instanceof Error && e.name !== "AbortError") {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(prev => {
|
||||||
|
// Remove the `id` key from `isLoading` when its value becomes `false`
|
||||||
|
const { [id]: _, ...rest } = prev;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [url, query, limit, searchParams]);
|
}, [url, query, limit, searchParams]);
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
||||||
## tip
|
## tip
|
||||||
|
|
||||||
* FEATURE: add an ability to specify extra fields for logs ingested via [HTTP-based data ingestion protocols](https://docs.victoriametrics.com/victorialogs/data-ingestion/#http-apis). See `extra_fields` query arg and `VL-Extra-Fields` HTTP header in [these docs](https://docs.victoriametrics.com/victorialogs/data-ingestion/#http-parameters).
|
* FEATURE: add an ability to specify extra fields for logs ingested via [HTTP-based data ingestion protocols](https://docs.victoriametrics.com/victorialogs/data-ingestion/#http-apis). See `extra_fields` query arg and `VL-Extra-Fields` HTTP header in [these docs](https://docs.victoriametrics.com/victorialogs/data-ingestion/#http-parameters).
|
||||||
|
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add frontend-only pagination for table view.
|
||||||
|
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): improve memory consumption during data processing. This enhancement reduces the overall memory footprint, leading to better performance and stability.
|
||||||
|
|
||||||
|
* OPTIMIZATION: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): reduced memory usage across all tabs for improved performance and stability. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7185).
|
||||||
|
|
||||||
## [v0.40.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.40.0-victorialogs)
|
## [v0.40.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.40.0-victorialogs)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue