From bb3cadc0070d2dc166a920b84f6e010508410ba3 Mon Sep 17 00:00:00 2001 From: Joel Guerra Date: Fri, 20 Sep 2024 17:30:49 +0200 Subject: [PATCH] feat: add metric charts --- packages/main/package.json | 1 + .../plugins/WebVitals/helper/webVitals.ts | 3 + .../plugins/WebVitals/performanceAxios.ts | 153 ++++++++++++++++++ packages/main/sections/Queries/QueryItem.tsx | 7 +- .../DataViews/components/Logs/LogRow.tsx | 10 +- .../DataViews/components/Logs/Row.tsx | 20 +-- .../DataViews/components/Logs/styled.tsx | 28 ++-- .../DataViews/components/Logs/types.tsx | 1 + .../components/ValueTags/MetricsChart.tsx | 102 ++++++++++++ .../components/ValueTags/ValueTags.tsx | 5 +- .../components/ValueTags/ValueTagsCont.tsx | 80 ++++++++- .../components/ValueTags/parseUrl.ts | 27 ++++ packages/main/store/actions/getData.ts | 47 ++++-- pnpm-lock.yaml | 23 +++ 14 files changed, 460 insertions(+), 47 deletions(-) create mode 100644 packages/main/plugins/WebVitals/performanceAxios.ts create mode 100644 packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx create mode 100644 packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts diff --git a/packages/main/package.json b/packages/main/package.json index 4e2e6376..2d83df61 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -30,6 +30,7 @@ "dayjs": "^1.11.12", "deep-freeze": "^0.0.1", "dnd-core": "^16.0.1", + "echarts": "^5.5.1", "fuzzy": "^0.1.3", "immutability-helper": "^3.1.1", "isomorphic-dompurify": "^1.13.0", diff --git a/packages/main/plugins/WebVitals/helper/webVitals.ts b/packages/main/plugins/WebVitals/helper/webVitals.ts index d42a10f6..3ae42217 100644 --- a/packages/main/plugins/WebVitals/helper/webVitals.ts +++ b/packages/main/plugins/WebVitals/helper/webVitals.ts @@ -99,6 +99,8 @@ const format_logs_queue = async (queue: QueueItem[]) => { level: "info", job: "webVitals", name: metric.name, + metricName: metric.name, + metricLabel: "page", description: MetricDescription[ metric.name as keyof typeof MetricDescription @@ -108,6 +110,7 @@ const format_logs_queue = async (queue: QueueItem[]) => { delta: metric.delta?.toString() || "N/A", traceId: traceId, page: page, + hasMetrics:true, }, values: [[String(Date.now() * 1000000), logString]], }; diff --git a/packages/main/plugins/WebVitals/performanceAxios.ts b/packages/main/plugins/WebVitals/performanceAxios.ts new file mode 100644 index 00000000..ac89d22b --- /dev/null +++ b/packages/main/plugins/WebVitals/performanceAxios.ts @@ -0,0 +1,153 @@ +import axios, { AxiosResponse } from "axios"; + +import { LOKI_WRITE, METRICS_WRITE, TEMPO_WRITE } from "./helper/webVitals"; +import { v4 as uuidv4 } from "uuid"; + +interface PerformanceEntry { + name: string; + startTime: number; + duration: number; + type?:string; + level?:string; + method: string; + url: string; + status: number; + traceId: string; +} + +const performanceQueue: Set = new Set(); + +export async function flushPerformanceQueue() { + if (performanceQueue.size === 0) return; + + const entries = Array.from(performanceQueue); + + // Format metrics + const metricsData = entries + .map( + (entry) => + `http_request,method=${entry.method},type=${entry.type},status=${entry.status} duration=${Math.round(entry.duration)} ${Date.now() * 1000000}`, + ) + .join("\n"); + + console.log(metricsData) + + // Format logs + const logsData = JSON.stringify({ + streams: entries.map((entry) => ({ + stream: { + level: entry.level ?? "info", + job: "httpRequest", + metricName:"http_request", + metricLabel:"type", + type: entry.type, + method: entry.method, + url: entry.url, + status: entry.status.toString(), + traceId: entry.traceId, + }, + values: [ + [ + String(Date.now() * 1000000), + `HTTP ${entry.method} ${entry.url} completed in ${entry.duration}ms with status ${entry.status}`, + ], + ], + })), + }); + + // Format traces + const tracesData = entries.map((entry) => ({ + id: uuidv4().replace(/-/g, ""), + traceId: entry.traceId, + name: `HTTP ${entry.type}`, + + timestamp:Math.floor(Date.now() * 1000), // microseconds + duration: Math.floor(entry.duration * 1000) , // microseconds + tags: { + "http.method": entry.method, + "http.url": entry.url, + "http.status_code": entry.status.toString(), + }, + localEndpoint: { + serviceName: "httpClient", + }, + })); + + console.log(tracesData) + + try { + await Promise.all([ + fetch(METRICS_WRITE, { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: metricsData, + }), + fetch(LOKI_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: logsData, + }), + fetch(TEMPO_WRITE, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(tracesData), + }), + ]); + } catch (error) { + console.error("Error flushing performance queue:", error); + + } + + performanceQueue.clear(); +} + + + +export function performanceAxios( + config: any, +): Promise { + const startTime = performance.now(); + const traceId = uuidv4().replace(/-/g, ""); +console.log(config) + return axios(config) + .then((response) => { + const endTime = performance.now(); + const duration = endTime - startTime; + + const entry: PerformanceEntry = { + name: `${config.method} ${config.url}`, + type:config.type, + level:"info", + startTime, + duration, + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: response.status, + traceId, + }; + + performanceQueue.add(entry); + + return response; + }) + .catch((error) => { + const endTime = performance.now(); + const duration = endTime - startTime; + + const entry: PerformanceEntry = { + name: `${config.method} ${config.url} (failed)`, + startTime, + duration, + type: config.type, + level:'error', + method: config.method?.toUpperCase() || "GET", + url: config.url || "", + status: error.response?.status || 0, + traceId, + }; + + performanceQueue.add(entry); + + throw error; + }); +} diff --git a/packages/main/sections/Queries/QueryItem.tsx b/packages/main/sections/Queries/QueryItem.tsx index b52124ea..95cc97cc 100644 --- a/packages/main/sections/Queries/QueryItem.tsx +++ b/packages/main/sections/Queries/QueryItem.tsx @@ -18,7 +18,7 @@ import { getStoredQueries, setStoredQuery, setLocalTabsState, - getLocalTabsState + getLocalTabsState, } from "./helpers"; import { useIdRefs } from "./hooks"; @@ -46,6 +46,8 @@ function TabPanel(props: TabPanelProps) { const QueryItem = (props: any) => { const { name, data } = props; + + console.log(data) const { id } = data; const [launchQuery, setLaunchQuery] = useState(""); const dispatch: any = useDispatch(); @@ -64,7 +66,6 @@ const QueryItem = (props: any) => { const deleteStoredQuery = (): void => { const prevStored = getStoredQueries(); - if (prevStored?.length > 0) { const filtered = filterLocal(prevStored, id); setStoredQuery(filtered); @@ -73,9 +74,7 @@ const QueryItem = (props: any) => { const onDeleteQuery = (): void => { const filtered = filterPanel(panelSelected, id); - const viewFiltered = filterPanel(dataView, id); - const prevStoredQuery = getStoredQueries(); if (prevStoredQuery?.length > 0) { diff --git a/packages/main/src/components/DataViews/components/Logs/LogRow.tsx b/packages/main/src/components/DataViews/components/Logs/LogRow.tsx index 4d6ba897..9b42fe70 100644 --- a/packages/main/src/components/DataViews/components/Logs/LogRow.tsx +++ b/packages/main/src/components/DataViews/components/Logs/LogRow.tsx @@ -1,11 +1,10 @@ - import { RowLogContent, RowTimestamp } from "./styled"; import { ILogRowProps } from "./types"; /** * Returns a Log Row with the row timestamp and text - * @param param0 - * @returns + * @param param0 + * @returns */ export function LogRow({ text, @@ -13,12 +12,13 @@ export function LogRow({ isMobile, isSplit, isShowTs, + onRowClick, }: ILogRowProps) { const showTimestamp = () => isShowTs && !isMobile && !isSplit; const dateFormatted = () => (isMobile || isSplit) && isShowTs; return ( -
+
{showTimestamp() && {dateFormated}} {dateFormatted() &&

{dateFormated}

} @@ -26,4 +26,4 @@ export function LogRow({
); -} \ No newline at end of file +} diff --git a/packages/main/src/components/DataViews/components/Logs/Row.tsx b/packages/main/src/components/DataViews/components/Logs/Row.tsx index 7581cdeb..f288be25 100644 --- a/packages/main/src/components/DataViews/components/Logs/Row.tsx +++ b/packages/main/src/components/DataViews/components/Logs/Row.tsx @@ -47,17 +47,17 @@ export function Row(props: IRowProps) { dataSourceData, }; - const rowProps = { - rowColor, - onClick: () => { - toggleItemActive(index); - }, - }; - return ( - - - + + toggleItemActive(index)} + {...logRowProps} + isShowTs={actQuery.isShowTs} + /> + toggleItemActive(index)} + {...valueTagsProps} + /> ); } diff --git a/packages/main/src/components/DataViews/components/Logs/styled.tsx b/packages/main/src/components/DataViews/components/Logs/styled.tsx index 820f07d0..958e44e8 100644 --- a/packages/main/src/components/DataViews/components/Logs/styled.tsx +++ b/packages/main/src/components/DataViews/components/Logs/styled.tsx @@ -1,5 +1,6 @@ import styled from "@emotion/styled"; -import {css} from '@emotion/css'; +import { css } from "@emotion/css"; +import { type QrynTheme } from "@ui/theme/types"; export const FlexWrap = css` display: flex; @@ -7,9 +8,11 @@ export const FlexWrap = css` margin-top: 3px; `; - -export const LogRowStyled = styled.div` - color: ${({theme}: any) => theme.contrast}; +export const LogRowStyled: any = styled.div<{ + theme: QrynTheme; + rowColor: any; +}>` + color: ${({ theme }) => theme.contrast}; font-size: 12px; cursor: pointer; padding-left: 0.5rem; @@ -20,7 +23,7 @@ export const LogRowStyled = styled.div` margin-top: 2px; font-family: monospace; &:hover { - background: ${({theme}: any) => theme.activeBg}; + background: ${({ theme }: any) => theme.activeBg}; } p { @@ -28,21 +31,28 @@ export const LogRowStyled = styled.div` overflow-wrap: anywhere; margin-left: 3px; } - border-left: 4px solid ${({rowColor}: any) => rowColor}; + border-left: 4px solid ${({ rowColor }: any) => rowColor}; .log-ts-row { display: flex; } + .value-tags-close { + height: 12px; + &:hover { + background: ${({ theme }) => theme.shadow}; + border-radius: 3px 3px 0px 0px; + } + } `; export const RowLogContent = styled.span` font-size: 12px; - color: ${({theme}: any) => theme.hardContrast}; + color: ${({ theme }: any) => theme.hardContrast}; line-height: 1.5; `; export const RowTimestamp = styled.span` position: relative; - color: ${({theme}: any) => theme.contrast}; + color: ${({ theme }: any) => theme.contrast}; margin-right: 0.25rem; white-space: nowrap; font-size: 12px; @@ -54,5 +64,3 @@ export const RowsCont = styled.div` overflow-y: auto; height: calc(100% - 20px); `; - - diff --git a/packages/main/src/components/DataViews/components/Logs/types.tsx b/packages/main/src/components/DataViews/components/Logs/types.tsx index b488af33..a21523d2 100644 --- a/packages/main/src/components/DataViews/components/Logs/types.tsx +++ b/packages/main/src/components/DataViews/components/Logs/types.tsx @@ -4,6 +4,7 @@ export interface ILogRowProps { isSplit: boolean; isMobile: boolean; isShowTs: boolean; + onRowClick : () => void } export interface IRowProps { diff --git a/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx b/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx new file mode 100644 index 00000000..8b973456 --- /dev/null +++ b/packages/main/src/components/DataViews/components/ValueTags/MetricsChart.tsx @@ -0,0 +1,102 @@ +import * as echarts from "echarts"; + +import React, { useRef, useEffect } from "react"; + +import dayjs from "dayjs"; + +type EChartsOption = echarts.EChartsOption; +export type MetricsChartProps = { + metricsData: any; + title: string; +}; + +export const MetricsChart: React.FC = ({ + metricsData, + title, +}) => { + const chartRef = useRef(null); + useEffect(() => { + if (metricsData?.length > 0 && chartRef.current) { + const myChart = echarts.init(chartRef.current); + + const formatMetricsData = (metricsData: any[]) => { + const series = []; + + for (let metric of metricsData) { + const entry = metric?.values?.reduce( + (acc, [date, data]) => { + acc.date.push( + dayjs(date * 1000).format("MM/DD HH:mm:ss") + ); + acc.data.push(data); + return acc; + }, + { date: [], data: [] } + ); + series.push(entry); + } + return series; + }; + + const series = formatMetricsData(metricsData); + const option: EChartsOption = { + width: chartRef.current.clientWidth - 85, + grid: { + left: 50, + }, + tooltip: { + trigger: "axis", + position: function (pt) { + return [pt[0], "10%"]; + }, + }, + title: { + left: "center", + text: title, + }, + toolbox: { + feature: { + dataZoom: { + yAxisIndex: "none", + }, + restore: {}, + saveAsImage: {}, + }, + }, + xAxis: { + type: "category", + boundaryGap: false, + data: series[0].date, + }, + yAxis: { + type: "value", + boundaryGap: [0, "100%"], + }, + dataZoom: [ + { + type: "inside", + start: 0, + end: 100, + }, + { + start: 0, + end: 100, + }, + ], + series: metricsData.map((metrics, index) => ({ + name: JSON.stringify(metrics.metric), + type: "line", + symbol: "none", + sampling: "none", + data: series[index].data, + })), + }; + + myChart.setOption(option); + } + }, [metricsData]); + + return ( +
+ ); +}; diff --git a/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx b/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx index d1436cba..be32ba8c 100644 --- a/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx +++ b/packages/main/src/components/DataViews/components/ValueTags/ValueTags.tsx @@ -7,7 +7,7 @@ import { useMediaQuery } from "react-responsive"; import { LinkButtonWithTraces } from "./LinkButtonWithTraces"; import { ValueTagsStyled } from "./styled"; import { FilterButtons } from "./FilterButtons"; -import useTheme from "@ui/theme/useTheme"; +import useTheme from "@ui/theme/useTheme"; /** * @@ -19,7 +19,6 @@ import useTheme from "@ui/theme/useTheme"; * @returns Component for the Tags for the Log rows */ - export default function ValueTags(props: any) { const { tags, actQuery, dataSourceData, linkedFieldTags } = props; const isTabletOrMobile = useMediaQuery({ query: "(max-width: 1013px)" }); @@ -90,7 +89,7 @@ export default function ValueTags(props: any) { }; return ( - {Object.entries(tags).map(([label, value]:[string,string], k) => ( + {Object.entries(tags).map(([label, value]: [string, string], k) => (
{!isEmbed && ( diff --git a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx index 9c158844..69ca62a2 100644 --- a/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx +++ b/packages/main/src/components/DataViews/components/ValueTags/ValueTagsCont.tsx @@ -1,18 +1,92 @@ import ValueTags from "./ValueTags"; +import { useState, useEffect } from "react"; +import { MetricsChart } from "./MetricsChart"; + /** - * - * @param props + * + * @param props * @returns Container for valueTags */ + +// this is the container for the labels inside each logs row + +export const KeyMetricLabels = { + webVitals: "page", + httpRequest: "type", +}; + +export const formatKeyLabels = (tags) => { + return { [KeyMetricLabels[tags.job]]: tags[KeyMetricLabels[tags.job]] }; +}; + export function ValueTagsCont(props: any) { - const { showLabels, tags, actQuery } = props; + const { onValueTagsClick, showLabels, tags, actQuery } = props; + const [logMetrics, setLogMetrics] = useState([]); + useEffect(() => { + if (tags?.metricName && tags?.metricLabel && showLabels) { + const url = constructRequestFromTags(tags.metricName, [ + tags.metricLabel, + tags[tags.metricLabel], + ]); + + const fetchMetrics = async () => + await fetch(url) + .then((data) => data.json()) + .then((res) => { + setLogMetrics(res.data.result); + }); + + fetchMetrics(); + } + }, [tags]); + + console.log(logMetrics, tags.hasMetrics); if (showLabels) { return (
+
+ {logMetrics?.length > 0 && + tags?.metricName && + tags?.metricLabel && ( + + )}
); } return null; } +// we could rebuild this with the relevant tags for each + +export function constructRequestFromTags(name, [key, val]) { + // Extract relevant properties from tags + //const { name, page } = tags; + + // Base URL + + const baseUrl =window.location.protocol + "//" + window.location.host + "/api/v1/query_range"; + + // Construct query parameter + const query = encodeURIComponent(`${name}{${key}="${val}"}`); + + // Time parameters + const end = Math.floor(Date.now() / 1000); // Current timestamp in seconds + const start = end - 24 * 60 * 60; // 24 hours ago + + // Other parameters + const limit = 100; + const step = 1; + const direction = "backwards"; + + // Construct the full URL + const url = `${baseUrl}?query=${query}&limit=${limit}&start=${start}&end=${end}&step=${step}&direction=${direction}`; + + return url; +} diff --git a/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts b/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts new file mode 100644 index 00000000..b089dd03 --- /dev/null +++ b/packages/main/src/components/DataViews/components/ValueTags/parseUrl.ts @@ -0,0 +1,27 @@ +export function parseURL(url) { + // Extract the hash fragment + const hashParts = url.split("#"); + const hashFragment = hashParts[hashParts.length - 1]; + + // Split the hash fragment into key-value pairs + const pairs = hashFragment.split("&"); + const params = {}; + + pairs.forEach((pair) => { + const [key, value] = pair.split("="); + if (key && value) { + // Decode the value and handle array-like params + let decodedValue = decodeURIComponent(value); + if (key === "left" || key === "right") { + try { + decodedValue = JSON.parse(decodedValue); + } catch (e) { + console.error("Error parsing JSON for", key, e); + } + } + params[key] = decodedValue; + } + }); + + return params; +} diff --git a/packages/main/store/actions/getData.ts b/packages/main/store/actions/getData.ts index b14bfb09..8e391908 100644 --- a/packages/main/store/actions/getData.ts +++ b/packages/main/store/actions/getData.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import store from "@ui/store/store"; +import store from "@ui/store/store"; import setIsEmptyView from "./setIsEmptyView"; import { getEndpoint } from "./helpers/getEP"; import { getQueryOptions } from "./helpers/getQueryOptions"; @@ -15,6 +15,12 @@ import { DataViews } from "../store.model"; import { setLeftDataView } from "./setLeftDataView"; import { setRightDataView } from "./setRightDataView"; +import { + performanceAxios, + flushPerformanceQueue, +} from "@ui/plugins/WebVitals/performanceAxios"; +//import { flushQueue } from './webVitals'; + /** * * @param queryInput the expression text @@ -28,7 +34,7 @@ import { setRightDataView } from "./setRightDataView"; // this one should load logs and metrics data // just change endpoint -function panelDispatch(panel: string, dispatch: Function, data: any) { +function panelDispatch(panel: string, dispatch: any, data: any) { if (panel === "left") { return dispatch(setLeftPanel(data)); } @@ -40,7 +46,7 @@ function panelDispatch(panel: string, dispatch: Function, data: any) { export function dataViewDispatch( panel: string, dataViews: DataViews, - dispatch: Function + dispatch: any ) { if (panel === "left") { return dispatch(setLeftDataView(dataViews)); @@ -164,7 +170,7 @@ export default function getData( ); const endpoint = getEndpoint(type, queryType, params); - const setLoading = (state: boolean, dispatch: Function) => { + const setLoading = (state: boolean, dispatch: any) => { const dataViews: DataViews = store.getState()?.[`${panel}DataView`]; const dataView = dataViews?.find((view) => view.id === id); if (dataView) { @@ -172,7 +178,7 @@ export default function getData( } dataViewDispatch(panel, dataViews, dispatch); }; - return async function (dispatch: Function) { + return async function (dispatch: any) { setLoading(true, dispatch); loadingState(dispatch, true); let cancelToken: any; @@ -202,8 +208,15 @@ export default function getData( try { if (options?.method === "POST") { - await axios - ?.post(endpoint, queryInput, options) + //await axios + await performanceAxios({ + method: "POST", + url: endpoint, + data: queryInput, + type, + ...options, + }) + // ?.post(endpoint, queryInput, options) ?.then((response) => { processResponse( type, @@ -226,13 +239,22 @@ export default function getData( .finally(() => { setLoading(false, dispatch); loadingState(dispatch, false); + flushPerformanceQueue(); // Flush the performance data + // flushQueue(webVitalsQueue); }); } else if (options?.method === "GET") { - await axios - ?.get(endpoint, { - auth: { username: user, password: pass }, - ...options, - }) + // await axios + // ?.get(endpoint, { + // auth: { username: user, password: pass }, + // ...options, + // }) + await performanceAxios({ + method: "GET", + url: endpoint, + type, + auth: { username: user, password: pass }, + ...options, + }) ?.then((response) => { processResponse( type, @@ -273,6 +295,7 @@ export default function getData( .finally(() => { loadingState(dispatch, false); setLoading(false, dispatch); + flushPerformanceQueue(); // Flush the performance data }); } } catch (e) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 114f515d..7eddf6f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,9 @@ importers: dnd-core: specifier: ^16.0.1 version: 16.0.1 + echarts: + specifier: ^5.5.1 + version: 5.5.1 fuzzy: specifier: ^0.1.3 version: 0.1.3 @@ -1532,6 +1535,9 @@ packages: dompurify@3.1.5: resolution: {integrity: sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==} + echarts@5.5.1: + resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} + electron-to-chromium@1.4.806: resolution: {integrity: sha512-nkoEX2QIB8kwCOtvtgwhXWy2IHVcOLQZu9Qo36uaGB835mdX/h8uLRlosL6QIhLVUnAiicXRW00PwaPZC74Nrg==} @@ -2761,6 +2767,9 @@ packages: '@swc/wasm': optional: true + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tss-react@4.9.10: resolution: {integrity: sha512-uQj+r8mOKy0tv+/GAIzViVG81w/WeTCOF7tjsDyNjlicnWbxtssYwTvVjWT4lhWh5FSznDRy6RFp0BDdoLbxyg==} peerDependencies: @@ -3065,6 +3074,9 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zrender@5.6.0: + resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zustand@4.5.2: resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} @@ -4438,6 +4450,11 @@ snapshots: dompurify@3.1.5: {} + echarts@5.5.1: + dependencies: + tslib: 2.3.0 + zrender: 5.6.0 + electron-to-chromium@1.4.806: {} entities@4.5.0: {} @@ -5769,6 +5786,8 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tslib@2.3.0: {} + tss-react@4.9.10(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.20(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@emotion/cache': 11.13.0 @@ -6019,6 +6038,10 @@ snapshots: zod@3.23.8: {} + zrender@5.6.0: + dependencies: + tslib: 2.3.0 + zustand@4.5.2(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1): dependencies: use-sync-external-store: 1.2.0(react@18.3.1)