mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-12 12:46:23 +01:00
vmui/logs: add markdown support (#6292)
Add support for markdown format and emoji for the `_msg` field in the "Group" view. Add markdown rendering toggle. Disabled by default. Value is stored in `localStorage`.
This commit is contained in:
parent
d269a95da3
commit
2300e30ff3
25
app/vmui/packages/vmui/package-lock.json
generated
25
app/vmui/packages/vmui/package-lock.json
generated
@ -11,7 +11,6 @@
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/marked": "^5.0.0",
|
||||
"@types/node": "^20.4.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react-input-mask": "^3.0.2",
|
||||
@ -22,7 +21,8 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^5.1.0",
|
||||
"marked": "^12.0.2",
|
||||
"marked-emoji": "^1.4.0",
|
||||
"preact": "^10.7.1",
|
||||
"qs": "^6.10.3",
|
||||
"react-input-mask": "^2.0.4",
|
||||
@ -4272,11 +4272,6 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg=="
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -13598,14 +13593,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz",
|
||||
"integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==",
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-emoji": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-1.4.0.tgz",
|
||||
"integrity": "sha512-/2TJfGzXpiBBq+X3akHHbTrAjZPJDwR+7FV6SyQLECnQEfaoVkrpKZJzHhPTAq3Sl/A1l2frMT0u6b38VBBlNg==",
|
||||
"peerDependencies": {
|
||||
"marked": ">=4 <13"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
|
@ -7,7 +7,6 @@
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/marked": "^5.0.0",
|
||||
"@types/node": "^20.4.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react-input-mask": "^3.0.2",
|
||||
@ -18,7 +17,8 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^5.1.0",
|
||||
"marked": "^12.0.2",
|
||||
"marked-emoji": "^1.4.0",
|
||||
"preact": "^10.7.1",
|
||||
"qs": "^6.10.3",
|
||||
"react-input-mask": "^2.0.4",
|
||||
|
@ -4,6 +4,7 @@ import AppContextProvider from "./contexts/AppContextProvider";
|
||||
import ThemeProvider from "./components/Main/ThemeProvider/ThemeProvider";
|
||||
import ExploreLogs from "./pages/ExploreLogs/ExploreLogs";
|
||||
import LogsLayout from "./layouts/LogsLayout/LogsLayout";
|
||||
import "./constants/markedPlugins";
|
||||
|
||||
const AppLogs: FC = () => {
|
||||
const [loadedTheme, setLoadedTheme] = useState(false);
|
||||
|
1915
app/vmui/packages/vmui/src/constants/emojis.ts
Normal file
1915
app/vmui/packages/vmui/src/constants/emojis.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
app/vmui/packages/vmui/src/constants/markedPlugins.ts
Normal file
5
app/vmui/packages/vmui/src/constants/markedPlugins.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { markedEmoji } from "marked-emoji";
|
||||
import { marked } from "marked";
|
||||
import emojis from "./emojis";
|
||||
|
||||
marked.use(markedEmoji({ emojis, renderer: (token) => token.emoji }));
|
@ -54,7 +54,7 @@ const useGetMetricsQL = () => {
|
||||
|
||||
const processMarkdown = (text: string) => {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = marked(text);
|
||||
div.innerHTML = marked(text) as string;
|
||||
const groups = div.querySelectorAll(`${CATEGORY_TAG}, ${FUNCTION_TAG}`);
|
||||
return processGroups(groups);
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ const ExploreLogs: FC = () => {
|
||||
const { logs, isLoading, error, fetchLogs } = useFetchLogs(serverUrl, query, limit);
|
||||
const [queryError, setQueryError] = useState<ErrorTypes | string>("");
|
||||
const [loaded, isLoaded] = useState(false);
|
||||
const [markdownParsing, setMarkdownParsing] = useState(getFromStorage("LOGS_MARKDOWN") === "true");
|
||||
|
||||
const handleRunQuery = () => {
|
||||
if (!query) {
|
||||
@ -51,6 +52,11 @@ const ExploreLogs: FC = () => {
|
||||
saveToStorage("LOGS_LIMIT", `${limit}`);
|
||||
};
|
||||
|
||||
const handleChangeMarkdownParsing = (val: boolean) => {
|
||||
saveToStorage("LOGS_MARKDOWN", `${val}`);
|
||||
setMarkdownParsing(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (query) handleRunQuery();
|
||||
}, [period]);
|
||||
@ -65,15 +71,18 @@ const ExploreLogs: FC = () => {
|
||||
query={query}
|
||||
error={queryError}
|
||||
limit={limit}
|
||||
markdownParsing={markdownParsing}
|
||||
onChange={setQuery}
|
||||
onChangeLimit={handleChangeLimit}
|
||||
onRun={handleRunQuery}
|
||||
onChangeMarkdownParsing={handleChangeMarkdownParsing}
|
||||
/>
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
<ExploreLogsBody
|
||||
data={logs}
|
||||
loaded={loaded}
|
||||
markdownParsing={markdownParsing}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -15,10 +15,12 @@ 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";
|
||||
|
||||
export interface ExploreLogBodyProps {
|
||||
data: Logs[];
|
||||
loaded?: boolean;
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
enum DisplayType {
|
||||
@ -33,7 +35,7 @@ const tabs = [
|
||||
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
|
||||
];
|
||||
|
||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
|
||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded, markdownParsing }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { timezone } = useTimeState();
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
@ -46,11 +48,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
|
||||
...item,
|
||||
_vmui_time: item._time ? dayjs(item._time).tz().format(`${DATE_TIME_FORMAT}.SSS`) : "",
|
||||
_vmui_data: JSON.stringify(item, null, 2),
|
||||
_vmui_markdown: 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"];
|
||||
const hideColumns = ["_vmui_data", "_vmui_time", "_vmui_markdown"];
|
||||
const keys = new Set<string>();
|
||||
for (const item of logs) {
|
||||
for (const key in item) {
|
||||
@ -125,6 +128,7 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, loaded }) => {
|
||||
<GroupLogs
|
||||
logs={logs}
|
||||
columns={columns}
|
||||
markdownParsing={markdownParsing}
|
||||
/>
|
||||
)}
|
||||
{activeTab === DisplayType.json && (
|
||||
|
@ -6,17 +6,29 @@ 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, onChange, onChangeLimit, onRun }) => {
|
||||
const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({
|
||||
query,
|
||||
limit,
|
||||
error,
|
||||
markdownParsing,
|
||||
onChange,
|
||||
onChangeLimit,
|
||||
onRun,
|
||||
onChangeMarkdownParsing,
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [errorLimit, setErrorLimit] = useState("");
|
||||
@ -66,6 +78,14 @@ const ExploreLogsHeader: FC<ExploreLogHeaderProps> = ({ query, limit, error, onC
|
||||
/>
|
||||
</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-helpful">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
|
@ -25,6 +25,10 @@
|
||||
justify-content: normal;
|
||||
}
|
||||
|
||||
&-contols {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__execute {
|
||||
display: grid;
|
||||
}
|
||||
|
@ -11,9 +11,10 @@ import GroupLogsItem from "./GroupLogsItem";
|
||||
interface TableLogsProps {
|
||||
logs: Logs[];
|
||||
columns: string[];
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
const GroupLogs: FC<TableLogsProps> = ({ logs }) => {
|
||||
const GroupLogs: FC<TableLogsProps> = ({ logs, markdownParsing }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
@ -77,6 +78,7 @@ const GroupLogs: FC<TableLogsProps> = ({ logs }) => {
|
||||
<GroupLogsItem
|
||||
key={`${value._msg}${value._time}`}
|
||||
log={value}
|
||||
markdownParsing={markdownParsing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -10,15 +10,16 @@ import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
log: Logs;
|
||||
markdownParsing: boolean;
|
||||
}
|
||||
|
||||
const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||
const GroupLogsItem: FC<Props> = ({ log, markdownParsing }) => {
|
||||
const {
|
||||
value: isOpenFields,
|
||||
toggle: toggleOpenFields,
|
||||
} = useBoolean(false);
|
||||
|
||||
const excludeKeys = ["_stream", "_msg", "_time", "_vmui_time", "_vmui_data"];
|
||||
const excludeKeys = ["_stream", "_msg", "_time", "_vmui_time", "_vmui_data", "_vmui_markdown"];
|
||||
const fields = Object.entries(log).filter(([key]) => !excludeKeys.includes(key));
|
||||
const hasFields = fields.length > 0;
|
||||
|
||||
@ -71,6 +72,7 @@ const GroupLogsItem: FC<Props> = ({ log }) => {
|
||||
"vm-group-logs-row-content__msg": true,
|
||||
"vm-group-logs-row-content__msg_missing": !log._msg
|
||||
})}
|
||||
dangerouslySetInnerHTML={markdownParsing && log._vmui_markdown ? { __html: log._vmui_markdown } : undefined}
|
||||
>
|
||||
{log._msg || "message missing"}
|
||||
</div>
|
||||
|
@ -96,6 +96,65 @@
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* styles for markdown */
|
||||
p, pre, code {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code:not(pre code), pre {
|
||||
display: inline-block;
|
||||
background: $color-hover-black;
|
||||
border: 1px solid $color-hover-black;
|
||||
border-radius: $border-radius-small;
|
||||
tab-size: 4;
|
||||
font-variant-ligatures: none;
|
||||
margin: calc($padding-small/4) 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: $font-family-global;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: $padding-small;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: $font-size-small;
|
||||
padding: calc($padding-small / 4) calc($padding-small / 2);
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-primary;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid $color-hover-black;
|
||||
margin: calc($padding-small/2) $padding-small;
|
||||
padding: calc($padding-small/2) $padding-small;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style-position: inside;
|
||||
}
|
||||
/* end styles for markdown */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
|
||||
<>
|
||||
<div>
|
||||
<span>Description:</span>
|
||||
<div dangerouslySetInnerHTML={{ __html: marked.parse(description) }}/>
|
||||
<div dangerouslySetInnerHTML={{ __html: marked(description) as string }}/>
|
||||
</div>
|
||||
<hr/>
|
||||
</>
|
||||
|
@ -7,6 +7,7 @@ export type StorageKeys = "AUTOCOMPLETE"
|
||||
| "DISABLED_DEFAULT_TIMEZONE"
|
||||
| "THEME"
|
||||
| "LOGS_LIMIT"
|
||||
| "LOGS_MARKDOWN"
|
||||
| "EXPLORE_METRICS_TIPS"
|
||||
| "QUERY_HISTORY"
|
||||
| "QUERY_FAVORITES"
|
||||
|
@ -19,6 +19,8 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
|
||||
|
||||
## tip
|
||||
|
||||
* FEATURE: [web UI](https://docs.victoriametrics.com/VictoriaLogs/querying/#web-ui): add markdown support to the `Group` view. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/6292).
|
||||
|
||||
## [v0.18.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.18.0-victorialogs)
|
||||
|
||||
Released at 2024-06-06
|
||||
|
Loading…
Reference in New Issue
Block a user