mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-12 12:46:23 +01:00
vmui/logs: improve UI functionality (#6688)
* add a toggle button to the "Group" tab that allows users to expand or collapse all groups at once
* introduce the ability to select a key for grouping logs within the "Group" tab
* display the number of entries within each log group.
* move the Markdown toggle to the general settings panel in the upper left corner.
(cherry picked from commit e06a19d85f
)
This commit is contained in:
parent
092ea42ba8
commit
00b108ca04
@ -13,6 +13,7 @@ import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { AppType } from "../../../types/appType";
|
||||
import SwitchMarkdownParsing from "../LogsSettings/MarkdownParsing/SwitchMarkdownParsing";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
@ -60,6 +61,10 @@ const GlobalSettings: FC = () => {
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: isLogsApp,
|
||||
component: <SwitchMarkdownParsing/>
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
component: <Timezones ref={timezoneSettingRef}/>
|
||||
|
@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $padding-medium;
|
||||
gap: $padding-large;
|
||||
width: 600px;
|
||||
padding-bottom: $padding-medium;
|
||||
|
||||
@ -39,6 +39,13 @@
|
||||
margin-bottom: $padding-global;
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding-top: $padding-small;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
&-url {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
@ -0,0 +1,35 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import Switch from "../../../Main/Switch/Switch";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import { useLogsDispatch, useLogsState } from "../../../../state/logsPanel/LogsStateContext";
|
||||
|
||||
const SwitchMarkdownParsing: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { markdownParsing } = useLogsState();
|
||||
const dispatch = useLogsDispatch();
|
||||
|
||||
|
||||
const handleChangeMarkdownParsing = (val: boolean) => {
|
||||
dispatch({ type: "SET_MARKDOWN_PARSING", payload: val });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="vm-server-configurator__title">
|
||||
Markdown Parsing for Logs
|
||||
</div>
|
||||
<Switch
|
||||
label={markdownParsing ? "Disable markdown parsing" : "Enable markdown parsing"}
|
||||
value={markdownParsing}
|
||||
onChange={handleChangeMarkdownParsing}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
<div className="vm-server-configurator__info">
|
||||
Toggle this switch to enable or disable the Markdown formatting for log entries.
|
||||
Enabling this will parse log texts to Markdown.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchMarkdownParsing;
|
@ -520,3 +520,25 @@ export const DownloadIcon = () => (
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ExpandIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M12 5.83 15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CollapseIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M7.41 18.59 8.83 20 12 16.83 15.17 20l1.41-1.41L12 14zm9.18-13.18L15.17 4 12 7.17 8.83 4 7.41 5.41 12 10z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
@ -6,11 +6,14 @@ import Tooltip from "../Main/Tooltip/Tooltip";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { useEffect } from "preact/compat";
|
||||
|
||||
type OrderDir = "asc" | "desc"
|
||||
|
||||
interface TableProps<T> {
|
||||
rows: T[];
|
||||
columns: { title?: string, key: keyof Partial<T>, className?: string }[];
|
||||
defaultOrderBy: keyof T;
|
||||
copyToClipboard?: keyof T;
|
||||
defaultOrderDir?: OrderDir;
|
||||
// TODO: Remove when pagination is implemented on the backend.
|
||||
paginationOffset: {
|
||||
startIndex: number;
|
||||
@ -18,9 +21,9 @@ interface TableProps<T> {
|
||||
}
|
||||
}
|
||||
|
||||
const Table = <T extends object>({ rows, columns, defaultOrderBy, copyToClipboard, paginationOffset }: TableProps<T>) => {
|
||||
const Table = <T extends object>({ rows, columns, defaultOrderBy, defaultOrderDir, copyToClipboard, paginationOffset }: TableProps<T>) => {
|
||||
const [orderBy, setOrderBy] = useState<keyof T>(defaultOrderBy);
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc");
|
||||
const [orderDir, setOrderDir] = useState<OrderDir>(defaultOrderDir || "desc");
|
||||
const [copied, setCopied] = useState<number | null>(null);
|
||||
|
||||
// const sortedList = useMemo(() => stableSort(rows as [], getComparator(orderDir, orderBy)),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { FC, useState } from "preact/compat";
|
||||
import Trace from "./Trace";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { ArrowDownIcon, CodeIcon, DeleteIcon, DownloadIcon } from "../Main/Icons";
|
||||
import { CodeIcon, CollapseIcon, DeleteIcon, DownloadIcon, ExpandIcon } from "../Main/Icons";
|
||||
import "./style.scss";
|
||||
import NestedNav from "./NestedNav/NestedNav";
|
||||
import Alert from "../Main/Alert/Alert";
|
||||
@ -89,13 +89,7 @@ const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDelete
|
||||
<Tooltip title={expandedTraces.includes(trace.idValue) ? "Collapse All" : "Expand All"}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={(
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-tracings-view-trace-header__expand-icon": true,
|
||||
"vm-tracings-view-trace-header__expand-icon_open": expandedTraces.includes(trace.idValue) })}
|
||||
><ArrowDownIcon/></div>
|
||||
)}
|
||||
startIcon={expandedTraces.includes(trace.idValue) ? <CollapseIcon/> : <ExpandIcon/> }
|
||||
onClick={handleExpandAll(trace)}
|
||||
ariaLabel={expandedTraces.includes(trace.idValue) ? "Collapse All" : "Expand All"}
|
||||
/>
|
||||
|
@ -3,10 +3,11 @@ import { TimeStateProvider } from "../state/time/TimeStateContext";
|
||||
import { QueryStateProvider } from "../state/query/QueryStateContext";
|
||||
import { CustomPanelStateProvider } from "../state/customPanel/CustomPanelStateContext";
|
||||
import { GraphStateProvider } from "../state/graph/GraphStateContext";
|
||||
import { DashboardsStateProvider } from "../state/dashboards/DashboardsStateContext";
|
||||
import { LogsStateProvider } from "../state/logsPanel/LogsStateContext";
|
||||
import { SnackbarProvider } from "./Snackbar";
|
||||
|
||||
import { combineComponents } from "../utils/combine-components";
|
||||
import { DashboardsStateProvider } from "../state/dashboards/DashboardsStateContext";
|
||||
|
||||
const providers = [
|
||||
AppStateProvider,
|
||||
@ -15,7 +16,8 @@ const providers = [
|
||||
CustomPanelStateProvider,
|
||||
GraphStateProvider,
|
||||
SnackbarProvider,
|
||||
DashboardsStateProvider
|
||||
DashboardsStateProvider,
|
||||
LogsStateProvider
|
||||
];
|
||||
|
||||
export default combineComponents(...providers);
|
||||
|
@ -31,7 +31,6 @@ const ExploreLogs: FC = () => {
|
||||
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
|
||||
const { fetchLogHits, ...dataLogHits } = useFetchLogHits(serverUrl, query);
|
||||
const [queryError, setQueryError] = useState<ErrorTypes | string>("");
|
||||
const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true");
|
||||
|
||||
const getPeriod = useCallback(() => {
|
||||
const relativeTimeOpts = relativeTimeOptions.find(d => d.id === relativeTime);
|
||||
@ -65,11 +64,6 @@ const ExploreLogs: FC = () => {
|
||||
saveToStorage("LOGS_LIMIT", `${limit}`);
|
||||
};
|
||||
|
||||
const handleChangeMarkdownParsing = (val: boolean) => {
|
||||
saveToStorage("LOGS_MARKDOWN", `${val}`);
|
||||
setMarkdownParsing(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (query) handleRunQuery();
|
||||
}, [periodState]);
|
||||
@ -84,11 +78,9 @@ const ExploreLogs: FC = () => {
|
||||
query={query}
|
||||
error={queryError}
|
||||
limit={limit}
|
||||
markdownParsing={markdownParsing}
|
||||
onChange={setQuery}
|
||||
onChangeLimit={handleChangeLimit}
|
||||
onRun={handleRunQuery}
|
||||
onChangeMarkdownParsing={handleChangeMarkdownParsing}
|
||||
/>
|
||||
{isLoading && <Spinner message={"Loading logs..."}/>}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
@ -100,10 +92,7 @@ const ExploreLogs: FC = () => {
|
||||
isLoading={isLoading ? false : dataLogHits.isLoading}
|
||||
/>
|
||||
)}
|
||||
<ExploreLogsBody
|
||||
data={logs}
|
||||
markdownParsing={markdownParsing}
|
||||
/>
|
||||
<ExploreLogsBody data={logs}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, useState, useMemo } 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 Tabs from "../../../components/Main/Tabs/Tabs";
|
||||
@ -19,7 +19,6 @@ import { marked } from "marked";
|
||||
|
||||
export interface ExploreLogBodyProps {
|
||||
data: Logs[];
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
enum DisplayType {
|
||||
@ -34,10 +33,11 @@ const tabs = [
|
||||
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
|
||||
];
|
||||
|
||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, markdownParsing }) => {
|
||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data }) => {
|
||||
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[]>([]);
|
||||
@ -100,6 +100,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, markdownParsing }) =>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === DisplayType.group && (
|
||||
<div
|
||||
className="vm-explore-logs-body-header__settings"
|
||||
ref={groupSettingsRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -123,7 +129,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, markdownParsing }) =>
|
||||
<GroupLogs
|
||||
logs={logs}
|
||||
columns={columns}
|
||||
markdownParsing={markdownParsing}
|
||||
settingsRef={groupSettingsRef}
|
||||
/>
|
||||
)}
|
||||
{activeTab === DisplayType.json && (
|
||||
|
@ -48,7 +48,8 @@ const TableLogs: FC<TableLogsProps> = ({ logs, displayColumns, tableCompact, col
|
||||
<Table
|
||||
rows={logs}
|
||||
columns={filteredColumns}
|
||||
defaultOrderBy={"_vmui_time"}
|
||||
defaultOrderBy={"_time"}
|
||||
defaultOrderDir={"desc"}
|
||||
copyToClipboard={"_vmui_data"}
|
||||
paginationOffset={{ startIndex: 0, endIndex: Infinity }}
|
||||
/>
|
||||
|
@ -6,28 +6,23 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import QueryEditor from "../../../components/Configurators/QueryEditor/QueryEditor";
|
||||
import TextField from "../../../components/Main/TextField/TextField";
|
||||
import Switch from "../../../components/Main/Switch/Switch";
|
||||
|
||||
export interface ExploreLogHeaderProps {
|
||||
query: string;
|
||||
limit: number;
|
||||
error?: string;
|
||||
markdownParsing: boolean;
|
||||
onChange: (val: string) => void;
|
||||
onChangeLimit: (val: number) => void;
|
||||
onRun: () => void;
|
||||
onChangeMarkdownParsing: (val: boolean) => void;
|
||||
}
|
||||
|
||||
const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
query,
|
||||
limit,
|
||||
error,
|
||||
markdownParsing,
|
||||
onChange,
|
||||
onChangeLimit,
|
||||
onRun,
|
||||
onChangeMarkdownParsing,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
@ -78,14 +73,7 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-logs-header-bottom">
|
||||
<div className="vm-explore-logs-header-bottom-contols">
|
||||
<Switch
|
||||
label={"Markdown parsing"}
|
||||
value={markdownParsing}
|
||||
onChange={onChangeMarkdownParsing}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-explore-logs-header-bottom-contols"></div>
|
||||
<div className="vm-explore-logs-header-bottom-helpful">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useMemo } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef } from "preact/compat";
|
||||
import { MouseEvent, useState } from "react";
|
||||
import "./style.scss";
|
||||
import { Logs } from "../../../api/types";
|
||||
@ -9,89 +9,213 @@ import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
import GroupLogsItem from "./GroupLogsItem";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import classNames from "classnames";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import { CollapseIcon, ExpandIcon, StorageIcon } from "../../../components/Main/Icons";
|
||||
import Popper from "../../../components/Main/Popper/Popper";
|
||||
import TextField from "../../../components/Main/TextField/TextField";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import useStateSearchParams from "../../../hooks/useStateSearchParams";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const WITHOUT_GROUPING = "No Grouping";
|
||||
|
||||
interface TableLogsProps {
|
||||
logs: Logs[];
|
||||
columns: string[];
|
||||
markdownParsing: boolean;
|
||||
settingsRef: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
|
||||
const GroupLogs: FC<TableLogsProps> = ({ logs, settingsRef }) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [expandGroups, setExpandGroups] = useState<boolean[]>([]);
|
||||
const [groupBy, setGroupBy] = useStateSearchParams("_stream", "groupBy");
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const [searchKey, setSearchKey] = useState("");
|
||||
const optionsButtonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
value: openOptions,
|
||||
toggle: toggleOpenOptions,
|
||||
setFalse: handleCloseOptions,
|
||||
} = useBoolean(false);
|
||||
|
||||
const expandAll = useMemo(() => expandGroups.every(Boolean), [expandGroups]);
|
||||
|
||||
const logsKeys = useMemo(() => {
|
||||
const excludeKeys = ["_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
||||
const uniqKeys = Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
|
||||
const keys = [WITHOUT_GROUPING, ...uniqKeys.filter(k => !excludeKeys.includes(k))];
|
||||
|
||||
if (!searchKey) return keys;
|
||||
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));
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}, [logs, searchKey]);
|
||||
|
||||
const groupData = useMemo(() => {
|
||||
return groupByMultipleKeys(logs, ["_stream"]).map((item) => {
|
||||
const streamValue = item.values[0]?._stream || "";
|
||||
const pairs = streamValue.slice(1, -1).match(/(?:[^\\,]+|\\,)+?(?=,|$)/g) || [streamValue];
|
||||
return groupByMultipleKeys(logs, [groupBy]).map((item) => {
|
||||
const streamValue = item.values[0]?.[groupBy] || "";
|
||||
const pairs = /^{.+}$/.test(streamValue)
|
||||
? streamValue.slice(1, -1).match(/(\\.|[^,])+/g) || [streamValue]
|
||||
: [streamValue];
|
||||
return {
|
||||
...item,
|
||||
pairs: pairs.filter(Boolean),
|
||||
};
|
||||
});
|
||||
}, [logs]);
|
||||
}, [logs, groupBy]);
|
||||
|
||||
const handleClickByPair = (pair: string) => async (e: MouseEvent<HTMLDivElement>) => {
|
||||
const handleClickByPair = (value: string) => async (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
const isCopied = await copyToClipboard(`${pair.replace(/=/, ": ")}`);
|
||||
const isKeyValue = /(.+)?=(".+")/.test(value);
|
||||
const copyValue = isKeyValue ? `${value.replace(/=/, ": ")}` : `${groupBy}: "${value}"`;
|
||||
const isCopied = await copyToClipboard(copyValue);
|
||||
if (isCopied) {
|
||||
setCopied(pair);
|
||||
setCopied(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectGroupBy = (key: string) => () => {
|
||||
setGroupBy(key);
|
||||
searchParams.set("groupBy", key);
|
||||
setSearchParams(searchParams);
|
||||
handleCloseOptions();
|
||||
};
|
||||
|
||||
const handleToggleExpandAll = useCallback(() => {
|
||||
setExpandGroups(new Array(groupData.length).fill(!expandAll));
|
||||
}, [expandAll]);
|
||||
|
||||
const handleChangeExpand = (i: number) => (value: boolean) => {
|
||||
setExpandGroups((prev) => {
|
||||
const newExpandGroups = [...prev];
|
||||
newExpandGroups[i] = value;
|
||||
return newExpandGroups;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (copied === null) return;
|
||||
const timeout = setTimeout(() => setCopied(null), 2000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [copied]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandGroups(new Array(groupData.length).fill(true));
|
||||
}, [groupData]);
|
||||
|
||||
return (
|
||||
<div className="vm-group-logs">
|
||||
{groupData.map((item) => (
|
||||
<div
|
||||
className="vm-group-logs-section"
|
||||
key={item.keys.join("")}
|
||||
>
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
title={(
|
||||
<div className="vm-group-logs-section-keys">
|
||||
<span className="vm-group-logs-section-keys__title">Group by _stream:</span>
|
||||
{item.pairs.map((pair) => (
|
||||
<Tooltip
|
||||
title={copied === pair ? "Copied" : "Copy to clipboard"}
|
||||
key={`${item.keys.join("")}_${pair}`}
|
||||
placement={"top-center"}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-group-logs-section-keys__pair": true,
|
||||
"vm-group-logs-section-keys__pair_dark": isDarkTheme
|
||||
})}
|
||||
onClick={handleClickByPair(pair)}
|
||||
<>
|
||||
<div className="vm-group-logs">
|
||||
{groupData.map((item, i) => (
|
||||
<div
|
||||
className="vm-group-logs-section"
|
||||
key={item.keys.join("")}
|
||||
>
|
||||
<Accordion
|
||||
key={String(expandGroups[i])}
|
||||
defaultExpanded={expandGroups[i]}
|
||||
onChange={handleChangeExpand(i)}
|
||||
title={groupBy !== WITHOUT_GROUPING && (
|
||||
<div className="vm-group-logs-section-keys">
|
||||
<span className="vm-group-logs-section-keys__title">Group by <code>{groupBy}</code>:</span>
|
||||
{item.pairs.map((pair) => (
|
||||
<Tooltip
|
||||
title={copied === pair ? "Copied" : "Copy to clipboard"}
|
||||
key={`${item.keys.join("")}_${pair}`}
|
||||
placement={"top-center"}
|
||||
>
|
||||
{pair}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-group-logs-section-keys__pair": true,
|
||||
"vm-group-logs-section-keys__pair_dark": isDarkTheme
|
||||
})}
|
||||
onClick={handleClickByPair(pair)}
|
||||
>
|
||||
{pair}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
<span className="vm-group-logs-section-keys__count">{item.values.length} entries</span>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="vm-group-logs-section-rows">
|
||||
{item.values.map((value) => (
|
||||
<GroupLogsItem
|
||||
key={`${value._msg}${value._time}`}
|
||||
log={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="vm-group-logs-section-rows">
|
||||
{item.values.map((value) => (
|
||||
<GroupLogsItem
|
||||
key={`${value._msg}${value._time}`}
|
||||
log={value}
|
||||
markdownParsing={markdownParsing}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{settingsRef.current && React.createPortal((
|
||||
<div className="vm-group-logs-header">
|
||||
<Tooltip title={expandAll ? "Collapse All" : "Expand All"}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={expandAll ? <CollapseIcon/> : <ExpandIcon/> }
|
||||
onClick={handleToggleExpandAll}
|
||||
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={"Group by"}>
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<StorageIcon/> }
|
||||
onClick={toggleOpenOptions}
|
||||
ariaLabel={"Group by"}
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</Tooltip>
|
||||
{
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
>
|
||||
<div className="vm-list vm-group-logs-header-keys">
|
||||
<div className="vm-group-logs-header-keys__search">
|
||||
<TextField
|
||||
label="Search key"
|
||||
value={searchKey}
|
||||
onChange={setSearchKey}
|
||||
type="search"
|
||||
/>
|
||||
</div>
|
||||
{logsKeys.map(id => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": id === groupBy
|
||||
})}
|
||||
key={id}
|
||||
onClick={handleSelectGroupBy(id)}
|
||||
>
|
||||
{id}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
), settingsRef.current)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,19 +7,21 @@ import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import { ArrowDownIcon, CopyIcon } from "../../../components/Main/Icons";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
import classNames from "classnames";
|
||||
import { useLogsState } from "../../../state/logsPanel/LogsStateContext";
|
||||
|
||||
interface Props {
|
||||
log: Logs;
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
|
||||
const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||
const {
|
||||
value: isOpenFields,
|
||||
toggle: toggleOpenFields,
|
||||
} = useBoolean(false);
|
||||
|
||||
const excludeKeys = ["_stream", "_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
||||
const { markdownParsing } = useLogsState();
|
||||
|
||||
const excludeKeys = ["_msg", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
||||
const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
|
||||
const hasFields = fields.length > 0;
|
||||
|
||||
|
@ -3,6 +3,22 @@
|
||||
.vm-group-logs {
|
||||
margin-top: calc(-1 * $padding-medium);
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: $padding-global;
|
||||
|
||||
&-keys {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
|
||||
&__search {
|
||||
padding: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-section {
|
||||
&-keys {
|
||||
display: flex;
|
||||
@ -14,6 +30,24 @@
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
&:before {
|
||||
content: "\"";
|
||||
}
|
||||
&:after {
|
||||
content: "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
padding-right: calc($padding-large * 3);
|
||||
}
|
||||
|
||||
&__pair {
|
||||
|
@ -0,0 +1,24 @@
|
||||
import React, { createContext, FC, useContext, useMemo, useReducer } from "preact/compat";
|
||||
import { LogsAction, LogsState, initialLogsState, reducer } from "./reducer";
|
||||
import { Dispatch } from "react";
|
||||
|
||||
type LogsStateContextType = { state: LogsState, dispatch: Dispatch<LogsAction> };
|
||||
|
||||
export const LogsStateContext = createContext<LogsStateContextType>({} as LogsStateContextType);
|
||||
|
||||
export const useLogsState = (): LogsState => useContext(LogsStateContext).state;
|
||||
export const useLogsDispatch = (): Dispatch<LogsAction> => useContext(LogsStateContext).dispatch;
|
||||
|
||||
export const LogsStateProvider: FC = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialLogsState);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return { state, dispatch };
|
||||
}, [state, dispatch]);
|
||||
|
||||
return <LogsStateContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</LogsStateContext.Provider>;
|
||||
};
|
||||
|
||||
|
26
app/vmui/packages/vmui/src/state/logsPanel/reducer.ts
Normal file
26
app/vmui/packages/vmui/src/state/logsPanel/reducer.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
|
||||
export interface LogsState {
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
export type LogsAction =
|
||||
| { type: "SET_MARKDOWN_PARSING", payload: boolean }
|
||||
|
||||
|
||||
export const initialLogsState: LogsState = {
|
||||
markdownParsing: getFromStorage("LOGS_MARKDOWN") === "true",
|
||||
};
|
||||
|
||||
export function reducer(state: LogsState, action: LogsAction): LogsState {
|
||||
switch (action.type) {
|
||||
case "SET_MARKDOWN_PARSING":
|
||||
saveToStorage("LOGS_MARKDOWN", `${ action.payload}`);
|
||||
return {
|
||||
...state,
|
||||
markdownParsing: action.payload
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
@ -17,6 +17,10 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
||||
## tip
|
||||
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add fields for setting AccountID and ProjectID. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6631).
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): add a toggle button to the "Group" tab that allows users to expand or collapse all groups at once.
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): introduce the ability to select a key for grouping logs within the "Group" tab.
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): display the number of entries within each log group.
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/victorialogs/querying/#web-ui): move the Markdown toggle to the general settings panel in the upper left corner.
|
||||
|
||||
## [v0.28.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.28.0-victorialogs)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user