From 10c42668a1455a0adaa1172ea12179d975fbb7cb Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Wed, 27 Nov 2024 13:49:06 +0100 Subject: [PATCH] vmui/logs: optimize memory consumption (#7524) ### Describe Your Changes - **Memory Optimization**: Reduced memory consumption on the "Group" and "JSON" tabs by approximately 30%. - **Table Pagination**: Added pagination to the "Table" view with an option to select the number of rows displayed (from 10 to 1000 items per page, with a default of 1000). This change significantly reduced memory usage by approximately 75%. Related to #7185 ### Checklist The following checks are **mandatory**: - [ ] My change adheres [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/contributing/). --------- Co-authored-by: Roman Khavronenko --- app/vmui/packages/vmui/.eslintrc.js | 1 + .../components/Main/Pagination/Pagination.tsx | 105 ++++++++++++++++++ .../PaginationControl/PaginationControl.tsx | 52 --------- .../Pagination/PaginationControl/style.scss | 24 ---- .../src/components/Main/Pagination/style.scss | 66 +++++++++++ .../vmui/src/components/Table/Table.tsx | 3 +- .../components/Views/JsonView/JsonView.tsx | 12 +- .../src/pages/ExploreLogs/ExploreLogs.tsx | 2 +- .../ExploreLogsBody/ExploreLogsBody.tsx | 50 +++++---- .../ExploreLogs/ExploreLogsBody/TableLogs.tsx | 92 ++++++++++----- .../pages/ExploreLogs/GroupLogs/GroupLogs.tsx | 32 +++--- .../GroupLogs/GroupLogsFieldRow.tsx | 54 +++++++++ .../ExploreLogs/GroupLogs/GroupLogsItem.tsx | 81 +++++--------- .../pages/ExploreLogs/hooks/useFetchLogs.ts | 17 +-- docs/VictoriaLogs/CHANGELOG.md | 4 + 15 files changed, 378 insertions(+), 217 deletions(-) create mode 100644 app/vmui/packages/vmui/src/components/Main/Pagination/Pagination.tsx delete mode 100644 app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/PaginationControl.tsx delete mode 100644 app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/style.scss create mode 100644 app/vmui/packages/vmui/src/components/Main/Pagination/style.scss create mode 100644 app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsFieldRow.tsx diff --git a/app/vmui/packages/vmui/.eslintrc.js b/app/vmui/packages/vmui/.eslintrc.js index 3f9b39389..32f8a0cc8 100644 --- a/app/vmui/packages/vmui/.eslintrc.js +++ b/app/vmui/packages/vmui/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { "@typescript-eslint" ], "rules": { + "@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }], "react/jsx-closing-bracket-location": [1, "line-aligned"], "react/jsx-max-props-per-line":[1, { "maximum": 1 }], "react/jsx-first-prop-new-line": [1, "multiline"], diff --git a/app/vmui/packages/vmui/src/components/Main/Pagination/Pagination.tsx b/app/vmui/packages/vmui/src/components/Main/Pagination/Pagination.tsx new file mode 100644 index 000000000..dd8820dbd --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Main/Pagination/Pagination.tsx @@ -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 = ({ + 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 ( +
+ + {pages.map((page, index) => ( + + ))} + +
+ ); +}; + +export default Pagination; diff --git a/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/PaginationControl.tsx b/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/PaginationControl.tsx deleted file mode 100644 index f849f57e5..000000000 --- a/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/PaginationControl.tsx +++ /dev/null @@ -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 = ({ page, length, limit, onChange }) => { - const { isMobile } = useDeviceDetect(); - - const handleChangePage = (step: number) => () => { - onChange(+page + step); - window.scrollTo(0, 0); - }; - - return ( -
- {page > 1 && ( -
} - > - Previous - - )} - {length >= limit && ( - - )} - - ); -}; - -export default PaginationControl; diff --git a/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/style.scss b/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/style.scss deleted file mode 100644 index fafcebe98..000000000 --- a/app/vmui/packages/vmui/src/components/Main/Pagination/PaginationControl/style.scss +++ /dev/null @@ -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); - } - } -} diff --git a/app/vmui/packages/vmui/src/components/Main/Pagination/style.scss b/app/vmui/packages/vmui/src/components/Main/Pagination/style.scss new file mode 100644 index 000000000..4b446f366 --- /dev/null +++ b/app/vmui/packages/vmui/src/components/Main/Pagination/style.scss @@ -0,0 +1,66 @@ +@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; + color: $color-text; + 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: $color-text-disabled; + } + + &:hover { + background-color: $color-hover-black; + } + } + + &__arrow { + + svg { + max-width: $font-size; + max-height: $font-size; + } + + &:disabled { + color: $color-text-disabled; + cursor: default; + pointer-events: none; + } + + &_prev { + transform: rotate(90deg); + } + + &_next { + transform: rotate(-90deg); + } + } +} diff --git a/app/vmui/packages/vmui/src/components/Table/Table.tsx b/app/vmui/packages/vmui/src/components/Table/Table.tsx index e4cf0ad77..5047590c6 100644 --- a/app/vmui/packages/vmui/src/components/Table/Table.tsx +++ b/app/vmui/packages/vmui/src/components/Table/Table.tsx @@ -32,8 +32,7 @@ const Table = ({ rows, columns, defaultOrderBy, defaultOrderDi const sortedList = useMemo(() => { const { startIndex, endIndex } = paginationOffset; return stableSort(rows as [], getComparator(orderDir, orderBy)).slice(startIndex, endIndex); - }, - [rows, orderBy, orderDir, paginationOffset]); + }, [rows, orderBy, orderDir, paginationOffset]); const createSortHandler = (key: keyof T) => () => { setOrderDir((prev) => prev === "asc" && orderBy === key ? "desc" : "asc"); diff --git a/app/vmui/packages/vmui/src/components/Views/JsonView/JsonView.tsx b/app/vmui/packages/vmui/src/components/Views/JsonView/JsonView.tsx index ef4933a0e..5c397cc4d 100644 --- a/app/vmui/packages/vmui/src/components/Views/JsonView/JsonView.tsx +++ b/app/vmui/packages/vmui/src/components/Views/JsonView/JsonView.tsx @@ -12,17 +12,7 @@ export interface JsonViewProps { const JsonView: FC = ({ data }) => { const copyToClipboard = useCopyToClipboard(); - const formattedJson = useMemo(() => { - 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 formattedJson = useMemo(() => JSON.stringify(data, null, 2), [data]); const handlerCopy = async () => { await copyToClipboard(formattedJson, "Formatted JSON has been copied"); diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx index 47827ccc7..198e66134 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogs.tsx @@ -54,7 +54,7 @@ const ExploreLogs: FC = () => { fetchLogs(newPeriod).then((isSuccess) => { isSuccess && !hideChart && fetchLogHits(newPeriod); }).catch(e => e); - setSearchParamsFromKeys( { + setSearchParamsFromKeys({ query, "g0.range_input": duration, "g0.end_input": newPeriod.date, diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx index da5584197..7ad6e238a 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/ExploreLogsBody.tsx @@ -1,22 +1,23 @@ 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 Tabs from "../../../components/Main/Tabs/Tabs"; import "./style.scss"; import classNames from "classnames"; import useDeviceDetect from "../../../hooks/useDeviceDetect"; import { Logs } from "../../../api/types"; -import dayjs from "dayjs"; -import { useTimeState } from "../../../state/time/TimeStateContext"; import useStateSearchParams from "../../../hooks/useStateSearchParams"; import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject"; import TableSettings from "../../../components/Table/TableSettings/TableSettings"; import useBoolean from "../../../hooks/useBoolean"; import TableLogs from "./TableLogs"; import GroupLogs from "../GroupLogs/GroupLogs"; -import { DATE_TIME_FORMAT } from "../../../constants/date"; -import { marked } from "marked"; +import JsonView from "../../../components/Views/JsonView/JsonView"; 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 { data: Logs[]; @@ -37,38 +38,35 @@ const tabs = [ const ExploreLogsBody: FC = ({ data, isLoading }) => { const { isMobile } = useDeviceDetect(); - const { timezone } = useTimeState(); const { setSearchParamsFromKeys } = useSearchParamsFromObject(); const groupSettingsRef = useRef(null); const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view"); const [displayColumns, setDisplayColumns] = useState([]); + const [rowsPerPage, setRowsPerPage] = useStateSearchParams(1000, "rows_per_page"); 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(() => { - if (!logs?.length) return []; - const hideColumns = ["_vmui_data", "_vmui_time", "_vmui_markdown"]; + if (!data?.length) return []; const keys = new Set(); - for (const item of logs) { + for (const item of data) { for (const key in item) { keys.add(key); } } - return Array.from(keys).filter((col) => !hideColumns.includes(col)); - }, [logs]); + return Array.from(keys); + }, [data]); const handleChangeTab = (view: string) => { setActiveTab(view as DisplayType); setSearchParamsFromKeys({ view }); }; + const handleSetRowsPerPage = (limit: number) => { + setRowsPerPage(limit); + setSearchParamsFromKeys({ rows_per_page: limit }); + }; + return (
= ({ data, isLoading }) => {
{activeTab === DisplayType.table && (
+ = ({ data, isLoading }) => { {!!data.length && ( <> {activeTab === DisplayType.table && ( - )} {activeTab === DisplayType.group && ( - )} {activeTab === DisplayType.json && ( - + )} )} diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/TableLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/TableLogs.tsx index b19ba52d0..2e7612878 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/TableLogs.tsx +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/ExploreLogsBody/TableLogs.tsx @@ -1,58 +1,94 @@ -import React, { FC, useMemo } from "preact/compat"; +import React, { FC, useMemo, useRef, useState } from "preact/compat"; import "./style.scss"; import Table from "../../../components/Table/Table"; import { Logs } from "../../../api/types"; +import Pagination from "../../../components/Main/Pagination/Pagination"; +import { useEffect } from "react"; interface TableLogsProps { logs: Logs[]; displayColumns: string[]; tableCompact: boolean; columns: string[]; + rowsPerPage: number; } -const TableLogs: FC = ({ logs, displayColumns, tableCompact, columns }) => { - const getColumnClass = (key: string) => { - switch (key) { - case "_time": - return "vm-table-cell_logs-time"; - case "_vmui_data": - return "vm-table-cell_logs vm-table-cell_pre"; - default: - return "vm-table-cell_logs"; - } - }; +const getColumnClass = (key: string) => { + switch (key) { + case "_time": + return "vm-table-cell_logs-time"; + default: + return "vm-table-cell_logs"; + } +}; + +const compactColumns = [{ + key: "_vmui_data", + title: "Data", + className: "vm-table-cell_logs vm-table-cell_pre" +}]; + +const TableLogs: FC = ({ logs, displayColumns, tableCompact, columns, rowsPerPage }) => { + const containerRef = useRef(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(() => { - if (tableCompact) { - return [{ - key: "_vmui_data", - title: "Data", - className: getColumnClass("_vmui_data") - }]; - } return columns.map((key) => ({ key: key as keyof Logs, title: key, className: getColumnClass(key), })); - }, [tableCompact, columns]); + }, [columns]); const filteredColumns = useMemo(() => { - if (tableCompact) return tableColumns; + if (tableCompact) return compactColumns; if (!displayColumns?.length) return []; return tableColumns.filter(c => displayColumns.includes(c.key as string)); }, [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 ( <> - +
+ + ); diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx index 83b77ecb6..6d87f7519 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogs.tsx @@ -20,13 +20,12 @@ import { getStreamPairs } from "../../../utils/logs"; const WITHOUT_GROUPING = "No Grouping"; -interface TableLogsProps { +interface Props { logs: Logs[]; - columns: string[]; settingsRef: React.RefObject; } -const GroupLogs: FC = ({ logs, settingsRef }) => { +const GroupLogs: FC = ({ logs, settingsRef }) => { const { isDarkTheme } = useAppState(); const copyToClipboard = useCopyToClipboard(); const [searchParams, setSearchParams] = useSearchParams(); @@ -46,19 +45,21 @@ const GroupLogs: FC = ({ logs, settingsRef }) => { const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]); 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 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 { const regexp = new RegExp(searchKey, "i"); - const found = keys.filter((item) => regexp.test(item)); - return found.sort((a,b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0)); + return logsKeys.filter(item => regexp.test(item)) + .sort((a, b) => (a.match(regexp)?.index || 0) - (b.match(regexp)?.index || 0)); } catch (e) { return []; } - }, [logs, searchKey]); + }, [logsKeys, searchKey]); const groupData = useMemo(() => { return groupByMultipleKeys(logs, [groupBy]).map((item) => { @@ -94,16 +95,15 @@ const GroupLogs: FC = ({ logs, settingsRef }) => { const handleToggleExpandAll = useCallback(() => { 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) => { const newExpandGroups = [...prev]; newExpandGroups[i] = value; return newExpandGroups; }); - - }; + }, []); useEffect(() => { if (copied === null) return; @@ -170,7 +170,7 @@ const GroupLogs: FC = ({ logs, settingsRef }) => { + + + + + ); +}; + +export default memo(GroupLogsFieldRow); diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx index 1136e760f..b14dbe02c 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/GroupLogs/GroupLogsItem.tsx @@ -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 "./style.scss"; import useBoolean from "../../../hooks/useBoolean"; -import Button from "../../../components/Main/Button/Button"; -import Tooltip from "../../../components/Main/Tooltip/Tooltip"; -import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons"; -import useCopyToClipboard from "../../../hooks/useCopyToClipboard"; +import { ArrowDownIcon } from "../../../components/Main/Icons"; import classNames from "classnames"; 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 { log: Logs; @@ -20,40 +22,31 @@ const GroupLogsItem: FC = ({ log }) => { } = useBoolean(false); const { markdownParsing } = useLogsState(); + const { timezone } = useTimeState(); - const excludeKeys = ["_msg", "_vmui_time", "_vmui_data", "_vmui_markdown"]; - const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key)); + const formattedTime = useMemo(() => { + 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 displayMessage = useMemo(() => { if (log._msg) return log._msg; 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; return obj; }, {}); return JSON.stringify(dataObject); }, [log, fields, hasFields]); - const copyToClipboard = useCopyToClipboard(); - const [copied, setCopied] = useState(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 (
= ({ log }) => {
- {log._vmui_time || "timestamp missing"} + {formattedTime || "timestamp missing"}
= ({ log }) => { "vm-group-logs-row-content__msg_empty-msg": !log._msg, "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 || "-"}
@@ -94,28 +87,12 @@ const GroupLogsItem: FC = ({ log }) => {
+
+ +
+
{field}{value}
- {fields.map(([key, value], i) => ( - ( + - - - - + field={key} + value={value} + /> ))}
-
- -
-
{key}{value}
@@ -125,4 +102,4 @@ const GroupLogsItem: FC = ({ log }) => { ); }; -export default GroupLogsItem; +export default memo(GroupLogsItem); diff --git a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts index 36dd4a888..3b45c3cd7 100644 --- a/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts +++ b/app/vmui/packages/vmui/src/pages/ExploreLogs/hooks/useFetchLogs.ts @@ -9,7 +9,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => { const [searchParams] = useSearchParams(); const [logs, setLogs] = useState([]); - const [isLoading, setIsLoading] = useState<{[key: number]: boolean;}>([]); + const [isLoading, setIsLoading] = useState<{ [key: number]: boolean }>({}); const [error, setError] = useState(); const abortControllerRef = useRef(new AbortController()); @@ -33,8 +33,9 @@ export const useFetchLogs = (server: string, query: string, limit: number) => { const parseLineToJSON = (line: string): Logs | null => { try { - return JSON.parse(line); + return line && JSON.parse(line); } catch (e) { + console.error(`Failed to parse "${line}" to JSON\n`, e); return null; } }; @@ -56,23 +57,25 @@ export const useFetchLogs = (server: string, query: string, limit: number) => { if (!response.ok || !response.body) { setError(text); setLogs([]); - setIsLoading(prev => ({ ...prev, [id]: false })); return false; } - const lines = text.split("\n").filter(line => line).slice(0, limit); - const data = lines.map(parseLineToJSON).filter(line => line) as Logs[]; + const data = text.split("\n", limit).map(parseLineToJSON).filter(line => line) as Logs[]; setLogs(data); - setIsLoading(prev => ({ ...prev, [id]: false })); return true; } catch (e) { - setIsLoading(prev => ({ ...prev, [id]: false })); if (e instanceof Error && e.name !== "AbortError") { setError(String(e)); console.error(e); setLogs([]); } 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]); diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md index 697e07825..8c9f9aa3f 100644 --- a/docs/VictoriaLogs/CHANGELOG.md +++ b/docs/VictoriaLogs/CHANGELOG.md @@ -15,6 +15,10 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta ## tip +* 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. +* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): reduce memory usage across all tabs for improved performance and stability. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7185). + ## [v1.0.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.0.0-victorialogs) Released at 2024-11-12