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"
|
||||
],
|
||||
"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"],
|
||||
|
|
|
@ -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 { 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");
|
||||
|
|
|
@ -12,17 +12,7 @@ export interface JsonViewProps {
|
|||
const JsonView: FC<JsonViewProps> = ({ 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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { timezone } = useTimeState();
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const groupSettingsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
|
||||
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
|
||||
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<string>();
|
||||
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 (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -97,6 +95,10 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
|||
</div>
|
||||
{activeTab === DisplayType.table && (
|
||||
<div className="vm-explore-logs-body-header__settings">
|
||||
<SelectLimit
|
||||
limit={rowsPerPage}
|
||||
onChange={handleSetRowsPerPage}
|
||||
/>
|
||||
<TableSettings
|
||||
columns={columns}
|
||||
selectedColumns={displayColumns}
|
||||
|
@ -124,22 +126,22 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
|||
{!!data.length && (
|
||||
<>
|
||||
{activeTab === DisplayType.table && (
|
||||
<TableLogs
|
||||
logs={logs}
|
||||
<MemoizedTableLogs
|
||||
logs={data}
|
||||
displayColumns={displayColumns}
|
||||
tableCompact={tableCompact}
|
||||
columns={columns}
|
||||
rowsPerPage={Number(rowsPerPage)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === DisplayType.group && (
|
||||
<GroupLogs
|
||||
logs={logs}
|
||||
columns={columns}
|
||||
<MemoizedGroupLogs
|
||||
logs={data}
|
||||
settingsRef={groupSettingsRef}
|
||||
/>
|
||||
)}
|
||||
{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 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<TableLogsProps> = ({ 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<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(() => {
|
||||
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 (
|
||||
<>
|
||||
<Table
|
||||
rows={logs}
|
||||
columns={filteredColumns}
|
||||
defaultOrderBy={"_time"}
|
||||
defaultOrderDir={"desc"}
|
||||
copyToClipboard={"_vmui_data"}
|
||||
paginationOffset={{ startIndex: 0, endIndex: Infinity }}
|
||||
<div ref={containerRef}>
|
||||
<Table
|
||||
rows={rows}
|
||||
columns={filteredColumns}
|
||||
defaultOrderBy={"_time"}
|
||||
defaultOrderDir={"desc"}
|
||||
copyToClipboard={"_vmui_data"}
|
||||
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";
|
||||
|
||||
interface TableLogsProps {
|
||||
interface Props {
|
||||
logs: Logs[];
|
||||
columns: string[];
|
||||
settingsRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||
const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
@ -46,19 +45,21 @@ const GroupLogs: FC<TableLogsProps> = ({ 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) => {
|
||||
|
@ -90,16 +91,15 @@ const GroupLogs: FC<TableLogsProps> = ({ 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;
|
||||
|
@ -166,7 +166,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
|||
<Tooltip title={expandAll ? "Collapse All" : "Expand All"}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/> }
|
||||
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/>}
|
||||
onClick={handleToggleExpandAll}
|
||||
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
|
||||
/>
|
||||
|
@ -175,7 +175,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
|||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<StorageIcon/> }
|
||||
startIcon={<StorageIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
ariaLabel={"Group by"}
|
||||
/>
|
||||
|
@ -197,7 +197,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
|||
type="search"
|
||||
/>
|
||||
</div>
|
||||
{logsKeys.map(id => (
|
||||
{filteredLogsKeys.map(id => (
|
||||
<div
|
||||
className={classNames({
|
||||
"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 "./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<Props> = ({ 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<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 (
|
||||
<div className="vm-group-logs-row">
|
||||
<div
|
||||
|
@ -74,10 +67,10 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
|||
<div
|
||||
className={classNames({
|
||||
"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
|
||||
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_missing": !displayMessage
|
||||
})}
|
||||
dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
|
||||
dangerouslySetInnerHTML={(markdownParsing && formattedMarkdown) ? { __html: formattedMarkdown } : undefined}
|
||||
>
|
||||
{displayMessage || "-"}
|
||||
</div>
|
||||
|
@ -94,28 +87,12 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
|||
<div className="vm-group-logs-row-fields">
|
||||
<table>
|
||||
<tbody>
|
||||
{fields.map(([key, value], i) => (
|
||||
<tr
|
||||
{fields.map(([key, value]) => (
|
||||
<GroupLogsFieldRow
|
||||
key={key}
|
||||
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 === 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>
|
||||
field={key}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</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 [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 abortControllerRef = useRef(new AbortController());
|
||||
|
||||
|
@ -35,6 +35,7 @@ export const useFetchLogs = (server: string, query: string, limit: number) => {
|
|||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse line to JSON", 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]);
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
|||
## 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: [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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue