From a9c5feb5e743634d89e1317147234aa91ee0d0b4 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 14 Sep 2023 10:21:07 +0800 Subject: [PATCH] [feat]: support exempalr --- go.mod | 2 +- go.sum | 4 +- http/server.go | 3 +- web/index.html | 2 +- web/package.json | 3 +- web/src/App.tsx | 4 +- web/src/components/SpanView.tsx | 133 --------------- web/src/components/StatsList.tsx | 29 ---- web/src/components/TraceFlame.tsx | 95 ----------- web/src/components/TraceSummary.tsx | 7 - web/src/components/dashboard/Dashboard.tsx | 103 ++++++++++++ web/src/components/dashboard/Panel.tsx | 19 ++- .../dashboard}/RowPanel.tsx | 0 .../{MetricExplore.tsx => DataExplore.tsx} | 22 ++- web/src/components/explore/QueryEditor.tsx | 106 ++++++------ web/src/components/index.ts | 12 +- web/src/components/input/DatasourceSelect.tsx | 68 +++++--- .../components/input/DatasourceSelectForm.tsx | 41 +++-- web/src/components/layout/FeatureMenu.tsx | 31 +++- .../view/dashboard/DashboardView.tsx | 49 ++++++ .../components/view/exemplar/ExemplarView.tsx | 154 ++++++++++++++++++ .../view/exemplar/exemplar-view.scss | 35 ++++ web/src/components/view/trace/TraceView.tsx | 146 +++++++++++++++++ .../view/trace/components/EventView.tsx | 40 +++++ .../view/trace/components/SpanHeader.tsx | 79 +++++++++ .../view/trace/components/SpanStatus.tsx | 41 +++++ .../view/trace/components/SpanType.tsx | 45 +++++ .../view/trace/components/SpanView.tsx | 137 ++++++++++++++++ .../view}/trace/flame/FlameView.tsx | 2 +- .../view}/trace/timeline/TimelineView.tsx | 83 +++++++--- .../view}/trace/trace-view.scss | 5 + web/src/constants/dashboard.ts | 2 + web/src/contexts/PanelEditContextProvider.tsx | 112 +++++++++++-- web/src/contexts/VariableContextProvider.tsx | 26 +-- web/src/features/chart/ChartDetail.tsx | 34 +++- web/src/features/dashboard/PanelEditor.tsx | 3 +- web/src/features/dashboard/View.tsx | 82 +--------- web/src/features/explore/Explore.tsx | 120 ++++---------- web/src/features/explore/module.ts | 12 +- .../setting/datasource/EditDataSource.tsx | 51 ++++-- .../setting/datasource/ListDataSource.tsx | 8 +- web/src/features/trace/TracePage.tsx | 34 ++++ web/src/features/trace/TraceView.tsx | 98 ----------- .../features/trace/components/SpanView.tsx | 124 -------------- web/src/hooks/use.trace.ts | 67 +++++--- .../datasources/lindb/SettingEditor.tsx | 1 + web/src/plugins/datasources/lindb/module.ts | 19 +++ .../plugins/datasources/lingo/QueryEditor.tsx | 66 ++++++++ web/src/plugins/datasources/lingo/module.ts | 16 +- .../plugins/datasources/lingo/query-edit.scss | 20 +++ .../visualizations/timeseries/TimeSeries.tsx | 8 +- .../timeseries/components/Menu.tsx | 84 +++++++--- .../timeseries/components/TimeSeriesChart.tsx | 71 +++++++- .../timeseries/components/chart.config.ts | 61 ++++++- web/src/services/index.ts | 1 - web/src/services/metric.service.ts | 41 ----- web/src/stores/annotation.store.ts | 36 ++++ web/src/stores/datasource.store.ts | 15 +- web/src/stores/exemplar.store.ts | 32 ++++ web/src/stores/index.ts | 3 + web/src/stores/trace.view.store.ts | 31 ++++ web/src/styles/layout.scss | 54 ++++++ web/src/types/annotation.ts | 30 ++++ web/src/types/datasource.ts | 12 ++ web/src/types/feature.ts | 10 +- web/src/types/index.ts | 1 + web/src/types/platform.ts | 5 + web/src/types/trace.ts | 25 ++- web/src/types/tracker.ts | 7 +- web/src/utils/dataset.ts | 33 +++- web/src/utils/datasource.ts | 51 ++++++ web/src/utils/index.ts | 2 + .../{features/trace/util => utils}/trace.ts | 59 ++++++- web/static/index.html | 6 +- web/yarn.lock | 13 +- 75 files changed, 2117 insertions(+), 969 deletions(-) delete mode 100644 web/src/components/SpanView.tsx delete mode 100644 web/src/components/StatsList.tsx delete mode 100644 web/src/components/TraceFlame.tsx delete mode 100644 web/src/components/TraceSummary.tsx create mode 100644 web/src/components/dashboard/Dashboard.tsx rename web/src/{features/dashboard/components => components/dashboard}/RowPanel.tsx (100%) rename web/src/components/explore/{MetricExplore.tsx => DataExplore.tsx} (69%) create mode 100644 web/src/components/view/dashboard/DashboardView.tsx create mode 100644 web/src/components/view/exemplar/ExemplarView.tsx create mode 100644 web/src/components/view/exemplar/exemplar-view.scss create mode 100644 web/src/components/view/trace/TraceView.tsx create mode 100644 web/src/components/view/trace/components/EventView.tsx create mode 100644 web/src/components/view/trace/components/SpanHeader.tsx create mode 100644 web/src/components/view/trace/components/SpanStatus.tsx create mode 100644 web/src/components/view/trace/components/SpanType.tsx create mode 100644 web/src/components/view/trace/components/SpanView.tsx rename web/src/{features => components/view}/trace/flame/FlameView.tsx (96%) rename web/src/{features => components/view}/trace/timeline/TimelineView.tsx (63%) rename web/src/{features => components/view}/trace/trace-view.scss (85%) create mode 100644 web/src/features/trace/TracePage.tsx delete mode 100644 web/src/features/trace/TraceView.tsx delete mode 100644 web/src/features/trace/components/SpanView.tsx create mode 100644 web/src/plugins/datasources/lingo/QueryEditor.tsx create mode 100644 web/src/plugins/datasources/lingo/query-edit.scss delete mode 100644 web/src/services/metric.service.ts create mode 100644 web/src/stores/annotation.store.ts create mode 100644 web/src/stores/exemplar.store.ts create mode 100644 web/src/stores/trace.view.store.ts create mode 100644 web/src/types/annotation.ts create mode 100644 web/src/utils/datasource.ts rename web/src/{features/trace/util => utils}/trace.ts (74%) diff --git a/go.mod b/go.mod index 650c5be..21efe32 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,7 @@ require ( github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/lindb/client_go v0.0.2 - github.com/lindb/common v0.0.2 + github.com/lindb/common v0.0.4 github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/microsoft/go-mssqldb v0.17.0 // indirect diff --git a/go.sum b/go.sum index 85bac97..9120fb8 100644 --- a/go.sum +++ b/go.sum @@ -185,8 +185,8 @@ github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lindb/client_go v0.0.2 h1:YoW/B7rSWLmHRn1yA3J837RyKBThZERXBKuDZ8/JB34= github.com/lindb/client_go v0.0.2/go.mod h1:WoS2VzbWjgKzOm7VGDpO126dXbp8zASCjY1X5aDgMFY= -github.com/lindb/common v0.0.2 h1:cxxAnZcIpIoG9iFsH/Md1q6J75hL7XOMXt5j6ZYTepc= -github.com/lindb/common v0.0.2/go.mod h1:LdGzS89fh2gFpsYsfLNvr2mccm8eOb8A4oNjNvorTyw= +github.com/lindb/common v0.0.4 h1:Avv0CYSDmK3j3s/yBezlia4sT+jHEo+8GTnnY0/k/Bo= +github.com/lindb/common v0.0.4/go.mod h1:LdGzS89fh2gFpsYsfLNvr2mccm8eOb8A4oNjNvorTyw= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= diff --git a/http/server.go b/http/server.go index 5ae0871..cae3a5f 100644 --- a/http/server.go +++ b/http/server.go @@ -26,6 +26,7 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/lindb/common/pkg/http/middleware" + "github.com/lindb/common/pkg/logger" "github.com/lindb/linsight/config" ) @@ -71,7 +72,7 @@ func (s *Server) initialize() { // Using middlewares on group. s.engine.Use(middleware.Recovery()) // use AccessLog to log panic error with zap - s.engine.Use(middleware.AccessLog()) + s.engine.Use(middleware.AccessLog(logger.GetLogger(logger.AccessLogModule, "HTTP"))) s.engine.Use(cors.Default()) // handle static resource handleStatic(s.engine) diff --git a/web/index.html b/web/index.html index 856ddaf..d02d34a 100644 --- a/web/index.html +++ b/web/index.html @@ -9,7 +9,7 @@ content="Web site created using create-react-app" /> - + Linsight diff --git a/web/package.json b/web/package.json index 793995d..b855cb6 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,7 @@ "@tanstack/react-query": "^4.16.1", "axios": "^1.3.4", "chart.js": "^4.0.1", - "chartjs-plugin-annotation": "^2.1.2", + "chartjs-plugin-annotation": "^3.0.1", "chokidar": "^3.5.3", "classnames": "^2.3.2", "cytoscape": "^3.23.0", @@ -40,6 +40,7 @@ "moment-timezone": "^0.5.43", "momentjs": "^2.0.0", "qs": "^6.11.0", + "re-resizable": "^6.9.11", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 9601ba7..c9439ce 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -23,7 +23,7 @@ import { FeatureRepositoryInst, Component as NavItem } from '@src/types'; import { Navigate, Route, Routes } from 'react-router-dom'; import Explore from './features/explore/Explore'; import User from './features/user/User'; -import TraceView from './features/trace/TraceView'; +import TracePage from './features/trace/TracePage'; const Content: React.FC = React.memo(() => { const { boot } = useContext(PlatformContext); @@ -56,7 +56,7 @@ const Content: React.FC = React.memo(() => { } errorElement={} /> {/* put /explore route to fix if routes is empty infinite loop*/} } errorElement={} /> - } errorElement={} /> + } errorElement={} /> } errorElement={} /> ); diff --git a/web/src/components/SpanView.tsx b/web/src/components/SpanView.tsx deleted file mode 100644 index 0d1aa05..0000000 --- a/web/src/components/SpanView.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { Card, Typography, SideSheet, Space, Tag, Tabs, TabPane, Descriptions, List, Divider } from '@douyinfe/semi-ui'; -import React from 'react'; -import * as _ from 'lodash-es'; -import moment from 'moment'; - -const { Text, Title } = Typography; - -const SpanView: React.FC<{ visible: boolean; setVisible: any; span: any; process: any; processes: any }> = (props) => { - const { visible, setVisible, span, process, processes } = props; - const logs = _.get(span, 'logs', []); - const renderTitle = () => { - if (!span) { - return null; - } - - const language = _.get( - _.find( - _.get(process, 'tags', []), - (o: any) => o.key === 'telemetry.sdk.language' || o.key === 'library.language' - ), - 'value', - '' - ); - const status = _.get( - _.find(_.get(span, 'tags', []), (o: any) => o.key === 'otel.status_code'), - 'value', - '' - ); - return ( -
- - - <Space> - {language && <i style={{ marginRight: 4 }} className={`devicon-${language}-plain colored`} />} - <Title heading={2}>{process.serviceName} - - {span.operationName} - - - - - } - description={ - - Start Time: - {moment(span.startTime / 1000).format('YYYY-MM-DD HH:mm:ss.SSS')} - Duration: - {span.duration / 1000} ms - Span ID: - {span.spanID} - - {status === 'ERROR' ? 'ERROR' : 'OK'} - - - } - /> -
- ); - }; - - return ( - setVisible(false)}> - - - - - - - - - Logs {logs.length} - - } - itemKey="3"> - ( - - - {[ - {moment(item.timestamp / 1000).format('YYYY-MM-DD HH:mm:ss.SSS')}, - ...item.fields.map((o: any, idx: number) => { - if (idx < item.fields.length) { - return ( - <> - - {o.key}: - {o.value} - - ); - } - return ( - <> - {o.key}: - {o.value} - - ); - }), - ]} - - - )} - /> - - - - - - - ); -}; - -export default SpanView; diff --git a/web/src/components/StatsList.tsx b/web/src/components/StatsList.tsx deleted file mode 100644 index dd887f7..0000000 --- a/web/src/components/StatsList.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IconSearch } from '@douyinfe/semi-icons'; -import { Card, Input, List } from '@douyinfe/semi-ui'; -import { MetricSrv } from '@src/services'; -import { TemplateKit } from '@src/utils'; -import { useQuery } from '@tanstack/react-query'; -import React from 'react'; - -const StatsList: React.FC<{ label: string }> = (props) => { - const { label } = props; - const { isLoading, data } = useQuery(['stats_list'], async () => { - await new Promise((r) => setTimeout(r, 1000)); - return MetricSrv.getStatsList(); - }); - console.log(isLoading, data); - - return ( - - } />} - dataSource={data || []} - renderItem={(item) => {TemplateKit.template(label, item.tags || {})}} - loading={isLoading} - /> - - ); -}; - -export default StatsList; diff --git a/web/src/components/TraceFlame.tsx b/web/src/components/TraceFlame.tsx deleted file mode 100644 index 4ccf173..0000000 --- a/web/src/components/TraceFlame.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { MutableRefObject, useEffect, useRef } from 'react'; -import FlameChart from 'flame-chart-js'; -import * as _ from 'lodash-es'; - -const TraceFlame: React.FC<{ spans: any }> = (props) => { - const { spans } = props; - const canvasRef = useRef() as MutableRefObject; - const containRef = useRef() as MutableRefObject; - - const buildFlameData = () => { - const rootStart = 1669255471995014; - const convertSpan = (span: any) => { - const start = span.startTime - rootStart; - _.set(span, 'start', start); - _.set(span, 'end', start + span.duration); - _.set(span, 'name', span.operationName); - _.set(span, 'type', span.operationName); - if (span.children) { - span.children.forEach((child: any) => { - convertSpan(child); - }); - span.children = _.sortBy(span.children, ['start']); - } - }; - - spans.forEach((span: any) => { - convertSpan(span); - }); - }; - // getWrapperWH = () => { - // const style = window.getComputedStyle(wrapper, null); - // - // return [parseInt(style.getPropertyValue('width')), parseInt(style.getPropertyValue('height')) - 3]; - // }; - useEffect(() => { - if (!canvasRef.current || !containRef.current || _.isEmpty(spans)) { - return; - } - const canvas = canvasRef.current; - canvas.width = containRef.current.getBoundingClientRect().width; - // canvas.height = height; - - buildFlameData(); - console.log(spans, '>>>'); - // spans[0].children = []; - new FlameChart({ - canvas: canvasRef.current, // mandatory - data: spans, - // data: [ - // { - // name: spans[0].name, - // start: spans[0].start, - // duration: spans[0].duration, - // children: [], - // // ...spans[0], - // }, - // { - // name: spans[0].name, - // start: spans[0].start + 100, - // duration: spans[0].duration, - // children: [], - // // ...spans[0], - // }, - // ], - // marks: [ - // { - // shortName: 'DCL', - // fullName: 'DOMContentLoaded', - // timestamp: 500, - // color: '#FFFFFF', - // }, - // ], - colors: { - task: '#FFFFFF', - 'sub-task': '#000000', - }, - settings: { - options: { - tooltip: () => { - /*...*/ - }, // see section "Custom Tooltip" below - timeUnits: 'μs', - }, - }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [spans]); - return ( -
- -
- ); -}; - -export default TraceFlame; diff --git a/web/src/components/TraceSummary.tsx b/web/src/components/TraceSummary.tsx deleted file mode 100644 index c78ef99..0000000 --- a/web/src/components/TraceSummary.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const TraceSummary: React.FC = () => { - return <>summary; -}; - -export default TraceSummary; diff --git a/web/src/components/dashboard/Dashboard.tsx b/web/src/components/dashboard/Dashboard.tsx new file mode 100644 index 0000000..6a91309 --- /dev/null +++ b/web/src/components/dashboard/Dashboard.tsx @@ -0,0 +1,103 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import React from 'react'; +import { + DefaultColumns, + DefaultRowHeight, + PanelGridPos, + RowPanelType, + VisualizationAddPanelType, +} from '@src/constants'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import { DashboardStore } from '@src/stores'; +import { PanelSetting } from '@src/types'; +import { pick } from 'lodash-es'; +import { AddPanelWidget, Panel, RowPanel } from '@src/components'; +const ReactGridLayout = WidthProvider(RGL); + +const Dashboard: React.FC<{ readonly?: boolean }> = (props) => { + const { readonly } = props; + const buildLayout = (panels: any) => { + const layout: any[] = []; + (panels || []).map((item: any, _index: number) => { + if (!item) { + return; + } + const gridPos = pick(item.gridPos, ['i', ...PanelGridPos]) as ReactGridLayout.Layout; + if (readonly) { + gridPos.static = true; + } else if (item.type === RowPanelType) { + gridPos.isResizable = false; + } + layout.push(gridPos); + }); + return layout; + }; + + const renderPanel = (panel: PanelSetting) => { + switch (panel.type) { + case VisualizationAddPanelType: + return ; + case RowPanelType: + return ; + default: + return ( + + ); + } + }; + + const renderPanels = () => { + const panels = DashboardStore.getPanels(); + return panels.map((item: PanelSetting, _index: number) => { + if (!item) { + return null; + } + return
{renderPanel(item)}
; + }); + }; + + const updatePanelGridPos = (layout: ReactGridLayout.Layout[]) => { + // NOTE: if use onLayoutChange will re-layout when component init + // and impact lazy load when toggle row + (layout || []).forEach((item: any) => { + const panel = DashboardStore.getPanel(parseInt(item.i)); + if (panel) { + DashboardStore.updatePanelConfig(panel, { gridPos: item }); + DashboardStore.sortPanels(); + } + }); + }; + + return ( + + {renderPanels()} + + ); +}; + +export default Dashboard; diff --git a/web/src/components/dashboard/Panel.tsx b/web/src/components/dashboard/Panel.tsx index 70c5d87..9d33f09 100644 --- a/web/src/components/dashboard/Panel.tsx +++ b/web/src/components/dashboard/Panel.tsx @@ -161,15 +161,17 @@ const PanelHeader = forwardRef( }}> Explore - {!isStatic && renderChartRepo()} - {isStatic && } {!isStatic && ( - } - type="danger" - onClick={() => DashboardStore.deletePanel(panel)}> - Remove - + <> + {renderChartRepo()} + + } + type="danger" + onClick={() => DashboardStore.deletePanel(panel)}> + Remove + + )} }> @@ -260,6 +262,7 @@ const ViewPanel: React.FC<{ const { panel, shortcutKey, isStatic, className, menu } = props; const [searchParams] = useSearchParams(); const navigate = useNavigate(); + // FIXME: maybe plugin not exist const plugin = VisualizationRepositoryInst.get(`${panel.type}`); const datasetType = plugin.getDataSetType(panel); const header = useRef(); diff --git a/web/src/features/dashboard/components/RowPanel.tsx b/web/src/components/dashboard/RowPanel.tsx similarity index 100% rename from web/src/features/dashboard/components/RowPanel.tsx rename to web/src/components/dashboard/RowPanel.tsx diff --git a/web/src/components/explore/MetricExplore.tsx b/web/src/components/explore/DataExplore.tsx similarity index 69% rename from web/src/components/explore/MetricExplore.tsx rename to web/src/components/explore/DataExplore.tsx index d13ded1..17acfe1 100644 --- a/web/src/components/explore/MetricExplore.tsx +++ b/web/src/components/explore/DataExplore.tsx @@ -18,23 +18,37 @@ under the License. import React, { useContext } from 'react'; import { DatasourceInstance } from '@src/types'; import { Card } from '@douyinfe/semi-ui'; +import { TraceView, QueryEditor } from '@src/components'; import Panel from '../dashboard/Panel'; import { PanelEditContext } from '@src/contexts'; -import { QueryEditor } from '..'; +import { get } from 'lodash-es'; +import { DatasourceKit } from '@src/utils'; -const MetricExplore: React.FC<{ datasource: DatasourceInstance }> = (props) => { +const DataExplore: React.FC<{ datasource: DatasourceInstance }> = (props) => { const { datasource } = props; const { panel } = useContext(PanelEditContext); + const renderContent = () => { + if (DatasourceKit.isTrace(datasource)) { + return ( + + ); + } + return ; + }; return ( <>
- + {renderContent()}
); }; -export default MetricExplore; +export default DataExplore; diff --git a/web/src/components/explore/QueryEditor.tsx b/web/src/components/explore/QueryEditor.tsx index ddf330c..a1720e6 100644 --- a/web/src/components/explore/QueryEditor.tsx +++ b/web/src/components/explore/QueryEditor.tsx @@ -26,7 +26,7 @@ import { IconChevronRight, } from '@douyinfe/semi-icons'; import { PanelEditContext, QueryEditContextProvider, TargetsContext, TargetsContextProvider } from '@src/contexts'; -import { DatasourceInstance, Query } from '@src/types'; +import { DatasourceCategory, DatasourceInstance, Query } from '@src/types'; import { cloneDeep, get, isEmpty } from 'lodash-es'; import Icon from '../common/Icon'; import './query.editor.scss'; @@ -40,7 +40,7 @@ import { DroppableStateSnapshot, } from 'react-beautiful-dnd'; import classNames from 'classnames'; -import { DNDKit } from '@src/utils'; +import { DatasourceKit, DNDKit } from '@src/utils'; import DatasourceSelectForm from '../input/DatasourceSelectForm'; import { MixedDatasource } from '@src/constants'; import { DatasourceStore } from '@src/stores'; @@ -60,6 +60,45 @@ const Targets: React.FC<{ datasource: DatasourceInstance }> = (props) => { updateTargetConfig, } = useContext(TargetsContext); + const renderActions = (target: Query, index: number) => { + if (!DatasourceKit.isMetric(defaultDatasource)) { + return null; + } + return ( + <> + + {DatasourceKit.isMetric(datasource) && ( + + )} ); }; diff --git a/web/src/components/index.ts b/web/src/components/index.ts index 313c63a..06facf2 100644 --- a/web/src/components/index.ts +++ b/web/src/components/index.ts @@ -41,15 +41,19 @@ export { default as LinSelect } from '@src/components/input/LinSelect'; export { default as IntegrationSelect } from '@src/components/input/IntegrationSelect'; export { default as QueryEditor } from '@src/components/explore/QueryEditor'; -export { default as MetricExplore } from '@src/components/explore/MetricExplore'; +export { default as DataExplore } from '@src/components/explore/DataExplore'; export { default as AddToCharts } from '@src/components/explore/AddToCharts'; export { default as AddToDashboard } from '@src/components/explore/AddToDashboard'; export { default as UnlinkChart } from '@src/components/dashboard/UnlinkChart'; export { default as AddPanelWidget } from '@src/components/dashboard/AddPanelWidget'; +export { default as RowPanel } from '@src/components/dashboard/RowPanel'; export { default as Panel } from '@src/components/dashboard/Panel'; +export { default as Dashboard } from '@src/components/dashboard/Dashboard'; + +// view +export { default as TraceView } from '@src/components/view/trace/TraceView'; +export { default as ExemplarView } from '@src/components/view/exemplar/ExemplarView'; +export { default as DashboardView } from '@src/components/view/dashboard/DashboardView'; -export { default as StatsList } from './StatsList'; -export { default as TraceFlame } from './TraceFlame'; export { default as TraceMap } from './TraceMap'; -export { default as TraceSummary } from './TraceSummary'; diff --git a/web/src/components/input/DatasourceSelect.tsx b/web/src/components/input/DatasourceSelect.tsx index f485065..4e8fc67 100644 --- a/web/src/components/input/DatasourceSelect.tsx +++ b/web/src/components/input/DatasourceSelect.tsx @@ -15,10 +15,10 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Divider, Form, Select, Typography } from '@douyinfe/semi-ui'; +import { Divider, Form, Select, Tag, Typography } from '@douyinfe/semi-ui'; import { DatasourceStore } from '@src/stores'; import React, { useContext } from 'react'; -import { isEmpty } from 'lodash-es'; +import { filter, includes, isEmpty } from 'lodash-es'; import { DatasourceInstance } from '@src/types'; import { PlatformContext } from '@src/contexts'; import { useNavigate } from 'react-router-dom'; @@ -32,20 +32,60 @@ const DatasourceSelect: React.FC<{ field?: string; label?: string; noLabel?: boolean; + multiple?: boolean; includeMixed?: boolean; + categories?: string[]; labelPosition?: 'top' | 'left' | 'inset'; rules?: RuleItem[]; style?: React.CSSProperties; }> = (props) => { - const { label, field, includeMixed, rules, labelPosition, noLabel, style } = props; + const { label, field, multiple, categories, includeMixed, rules, labelPosition, noLabel, style } = props; const datasources = DatasourceStore.getDatasources(includeMixed); const { theme } = useContext(PlatformContext); const navigate = useNavigate(); + const renderSelectedItem = (n: Record): any => { + const ds = DatasourceStore.getDatasource(n.value); + if (!ds) { + return null; + } + return ( +
+ {n.value === MixedDatasource ? ( + + ) : ( + + )} + {ds.setting.name} +
+ ); + }; + const renderMultipleSelectedItem = (n: Record, { onClose }: any): any => { + const ds = DatasourceStore.getDatasource(n.value); + if (!ds) { + return null; + } + const content = ( + + {n.value === MixedDatasource ? ( + + ) : ( + + )} + {ds.setting.name} + + ); + return { + isRenderInTag: false, + content, + }; + }; + return ( ) } - renderSelectedItem={(n: Record) => { - const ds = DatasourceStore.getDatasource(n.value); - if (!ds) { - return null; + renderSelectedItem={multiple ? renderMultipleSelectedItem : renderSelectedItem}> + {filter(datasources, (ds: DatasourceInstance) => { + if (isEmpty(categories)) { + return true; } - return ( -
- {n.value === MixedDatasource ? ( - - ) : ( - - )} - {ds.setting.name} -
- ); - }}> - {datasources.map((ds: DatasourceInstance) => { + return includes(categories, ds.plugin.category); + }).map((ds: DatasourceInstance) => { const setting = ds.setting; return ( diff --git a/web/src/components/input/DatasourceSelectForm.tsx b/web/src/components/input/DatasourceSelectForm.tsx index 84c4431..bfe7ab9 100644 --- a/web/src/components/input/DatasourceSelectForm.tsx +++ b/web/src/components/input/DatasourceSelectForm.tsx @@ -17,41 +17,53 @@ under the License. */ import { Form } from '@douyinfe/semi-ui'; import { DatasourceStore } from '@src/stores'; -import React, { MutableRefObject, useMemo, useRef } from 'react'; -import { DatasourceInstance } from '@src/types'; +import React, { MutableRefObject, useEffect, useMemo, useRef } from 'react'; +import { DatasourceInstance, Tracker } from '@src/types'; import { DatasourceSelect } from '@src/components'; const DatasourceSelectForm: React.FC<{ - value?: string | null; + value?: string; label?: string; noLabel?: boolean; + categories?: string[]; labelPosition?: 'top' | 'left' | 'inset'; includeMixed?: boolean; style?: React.CSSProperties; onChange?: (instance: DatasourceInstance) => void; }> = (props) => { - const { value, label, labelPosition, noLabel, includeMixed, style, onChange } = props; - const previousValue = useRef() as MutableRefObject; + const { value, label, categories, labelPosition, noLabel, includeMixed, style, onChange } = props; + const tracker = useRef() as MutableRefObject>; + const formApi = useRef(); useMemo(() => { - previousValue.current = `${value}`; + tracker.current = new Tracker(value); + }, [value]); // NOTE: just init + + useEffect(() => { + if (formApi.current) { + formApi.current.setValues({ datasource: value }); + } }, [value]); return (
{ + formApi.current = api; + }} onValueChange={(values: any) => { if (!onChange) { return; } + const value = values['datasource']; - if (value === previousValue.current) { - return; - } - previousValue.current = value; - const ds = DatasourceStore.getDatasource(value); - if (ds) { - onChange(ds); + + if (tracker.current.isChanged(value)) { + tracker.current.setNewVal(value); + + const ds = DatasourceStore.getDatasource(value); + if (ds) { + onChange(ds); + } } }}> diff --git a/web/src/components/layout/FeatureMenu.tsx b/web/src/components/layout/FeatureMenu.tsx index d8789c5..e2fa4cc 100644 --- a/web/src/components/layout/FeatureMenu.tsx +++ b/web/src/components/layout/FeatureMenu.tsx @@ -17,7 +17,7 @@ under the License. */ import { PlatformContext } from '@src/contexts'; import { MenuStore } from '@src/stores'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { isEmpty, get, upperFirst } from 'lodash-es'; import { Button, Dropdown, Modal, Nav, Table, Tag, Typography } from '@douyinfe/semi-ui'; @@ -26,7 +26,7 @@ import { Notification } from '@src/components'; import Icon from '../common/Icon'; import Sider from '@douyinfe/semi-ui/lib/es/layout/Sider'; import Logo from '@src/images/logo.svg'; -import { ThemeType, UserOrg } from '@src/types'; +import { FeatureRepositoryInst, ThemeType, UserOrg } from '@src/types'; import { UserSrv } from '@src/services'; import { matchPath } from 'react-router'; import { v4 as uuidv4 } from 'uuid'; @@ -138,6 +138,21 @@ const FeatureMenu: React.FC = () => { return menu; }; + const goto = useCallback( + (item: any) => { + const feature = FeatureRepositoryInst.getFeature(item.component); + let params = ''; + if (feature && feature.getDefaultSearchParams) { + params = feature.getDefaultSearchParams(); + } + navigate({ + pathname: item.path, + search: params, + }); + }, + [navigate] + ); + useEffect(() => { const renderMenus = (menus: any) => { return (menus || []).map((item: any) => { @@ -156,7 +171,7 @@ const FeatureMenu: React.FC = () => { icon={} text={child.label} itemKey={child.path || child.label} - onClick={() => navigate(child.path)} + onClick={() => goto(child)} /> ))} @@ -177,7 +192,7 @@ const FeatureMenu: React.FC = () => { itemKey={item.path || item.label} key={uuidv4()} text={item.label} - onClick={() => navigate(item.path)} + onClick={() => goto(item)} /> ); @@ -195,14 +210,14 @@ const FeatureMenu: React.FC = () => { }); }; setMenus(renderMenus(boot.navTree)); - }, [boot.navTree, collapsed, navigate]); + }, [boot.navTree, collapsed, navigate, goto]); return ( <> {visible && ( { setVisible(v); @@ -251,7 +266,7 @@ const FeatureMenu: React.FC = () => { setVisible(true); }}> Organization: - {currentOrg.name} + {get(currentOrg, 'name', 'N/A')} )} navigate('/user/profile')}>Your profile diff --git a/web/src/components/view/dashboard/DashboardView.tsx b/web/src/components/view/dashboard/DashboardView.tsx new file mode 100644 index 0000000..c6dcb47 --- /dev/null +++ b/web/src/components/view/dashboard/DashboardView.tsx @@ -0,0 +1,49 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Loading } from '@src/components'; +import { VariableContextProvider } from '@src/contexts'; +import { useRequest } from '@src/hooks'; +import { DashboardStore } from '@src/stores'; +import { Dashboard } from '@src/components'; +import { toJS } from 'mobx'; +import React from 'react'; + +const DashboardView: React.FC<{ dashboardId: string; initVariableValues?: object }> = (props) => { + const { dashboardId, initVariableValues } = props; + const { loading } = useRequest( + ['load-dashboard-dashboard-view', dashboardId], + async () => { + return DashboardStore.loadDashbaord(dashboardId); + }, + {} + ); + if (loading) { + return ( +
+ +
+ ); + } + return ( + + + + ); +}; + +export default DashboardView; diff --git a/web/src/components/view/exemplar/ExemplarView.tsx b/web/src/components/view/exemplar/ExemplarView.tsx new file mode 100644 index 0000000..2ef7c62 --- /dev/null +++ b/web/src/components/view/exemplar/ExemplarView.tsx @@ -0,0 +1,154 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Modal, TagGroup } from '@douyinfe/semi-ui'; +import { StatusTip, TraceView } from '@src/components'; +import { useRequest } from '@src/hooks'; +import { DataQuerySrv } from '@src/services'; +import { DatasourceStore, ExemplarStore, TraceViewStore } from '@src/stores'; +import { Exemplar, FormatRepositoryInst } from '@src/types'; +import { DataSetKit } from '@src/utils'; +import { cloneDeep, get, isEmpty, set, unset } from 'lodash-es'; +import { reaction } from 'mobx'; +import React, { useEffect, useState } from 'react'; +import './exemplar-view.scss'; + +const ExemplarView: React.FC = () => { + const [visible, setVisible] = useState(false); + const [exemplars, setExemplars] = useState([]); + const [selectExemplar, setSelectExemplar] = useState(null); + + const getMetricQueryTarget = () => { + return get(ExemplarStore.selectDataPoint, 'point.meta.target'); + }; + + const { loading, result, error } = useRequest( + ['load_exemplars', ExemplarStore.selectDataPoint], + () => { + const target = cloneDeep(getMetricQueryTarget()); + if (!target) { + return; + } + const metricDatasourceUID = get(target, 'datasource.uid', ''); + const metricDatasource = DatasourceStore.getDatasource(metricDatasourceUID); + if (!metricDatasource) { + return null; + } + set(target, 'request.fields', ['rpc_e']); + unset(target, 'request.groupBy'); + + const timestamp = get(ExemplarStore.selectDataPoint, 'timestamp', 0); + if (timestamp <= 0) { + return; + } + const interval = get(ExemplarStore.selectDataPoint, 'point.meta.interval', 0); + return DataQuerySrv.dataQuery({ + queries: [target], + range: { + from: timestamp, + to: timestamp + interval - 1, + }, + }); + }, + { enabled: !isEmpty(getMetricQueryTarget()) } + ); + + useEffect(() => { + const exemplars = DataSetKit.createExemplarDatasets(result); + setExemplars(exemplars); + setSelectExemplar(get(exemplars, '[0]', null)); + }, [result]); + + useEffect(() => { + const disposer = reaction( + () => ExemplarStore.selectDataPoint, + (datapoint: any | null) => { + if (datapoint) { + setVisible(true); + } else { + setVisible(false); + } + } + ); + + return () => disposer(); + }, []); + + const renderTrace = () => { + if (isEmpty(exemplars)) { + return
no data
; + } + const target = getMetricQueryTarget(); + const metricDatasourceUID = get(target, 'datasource.uid', ''); + const metricDatasource = DatasourceStore.getDatasource(metricDatasourceUID); + if (!metricDatasource) { + return
no metric datasource
; + } + const traceDatasources = get(metricDatasource.setting, 'config.traceDatasources', []); + if (isEmpty(traceDatasources)) { + return
no trace datasource
; + } + return ( + + ); + }; + + return ( + + { + const tagKey = item.traceId + item.spanId; + const selected = `${selectExemplar?.traceId}${selectExemplar?.spanId}` === tagKey; + return { + tagKey: tagKey, + children: `L(${FormatRepositoryInst.get('ns').formatString(item.duration, 3)})`, + style: { + background: selected ? 'var(--semi-color-primary)' : 'var(--semi-color-tertiary)', + }, + type: 'solid', + }; + })} + /> + + } + onCancel={() => { + if (TraceViewStore.viewVisibleSpanView) { + // if span view is visible, need close span viw + TraceViewStore.setVisibleSpanView(false); + } else { + setVisible(false); + } + }}> + {loading && } + {renderTrace()} + + ); +}; + +export default ExemplarView; diff --git a/web/src/components/view/exemplar/exemplar-view.scss b/web/src/components/view/exemplar/exemplar-view.scss new file mode 100644 index 0000000..c344f05 --- /dev/null +++ b/web/src/components/view/exemplar/exemplar-view.scss @@ -0,0 +1,35 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +.exemplar-modal { + .semi-modal-content { + padding: 0; + height: 100%; + overflow: auto; + } + + .semi-modal-header { + padding: 12px 8px; + margin: 0; + position: sticky; + top: 0; + z-index: 10; + background-color: var(--semi-color-bg-2); + border-bottom: 1px solid var(--semi-color-border); + box-shadow: 0 3px 2px -1px rgba(0, 0, 0, 10%); + } +} diff --git a/web/src/components/view/trace/TraceView.tsx b/web/src/components/view/trace/TraceView.tsx new file mode 100644 index 0000000..64c6972 --- /dev/null +++ b/web/src/components/view/trace/TraceView.tsx @@ -0,0 +1,146 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Button, Dropdown, Layout, Typography } from '@douyinfe/semi-ui'; +import { IconInheritStroked, IconDescend, IconFlowChartStroked, IconPieChart2Stroked } from '@douyinfe/semi-icons'; +import { useTrace } from '@src/hooks'; +import React from 'react'; +import TimelineView from './timeline/TimelineView'; +import { Query } from '@src/types'; +import FlameView from './flame/FlameView'; +import { cloneDeep, isEmpty } from 'lodash-es'; +import { Icon, Loading, StatusTip } from '@src/components'; +import './trace-view.scss'; +import { createSearchParams, useSearchParams } from 'react-router-dom'; +import SpanHeader from './components/SpanHeader'; + +const { Text } = Typography; +const { Header } = Layout; + +const TraceView: React.FC<{ + traceId?: string; + spanId?: string; + datasources?: string[]; + openSelectedSpan?: boolean; + newWindowLink?: boolean; +}> = (props) => { + const { traceId, spanId, datasources, openSelectedSpan, newWindowLink } = props; + const [searchParams, setSearchParams] = useSearchParams(); + const viewType = searchParams.get('view') || 'timeline'; + + const buildQueries = (): Query[] => { + return (datasources || []).map((ds: string) => { + return { + datasource: { uid: ds }, + request: { + traceId: traceId, + }, + }; + }); + }; + const { loading, traces, tree, spanMap } = useTrace(buildQueries()); + + // FIXME: add trace/datasouce empty check + if (loading) { + return ( +
+ +
+ ); + } + + if (isEmpty(traces) || isEmpty(tree)) { + // FIXME: no found page + return ( +
+ +
+ ); + } + + const renderContent = () => { + switch (viewType) { + case 'flame': + return ; + default: + return ; + } + }; + + const renderIcon = () => { + switch (viewType) { + case 'flame': + return ; + default: + return ; + } + }; + + const changeViewType = (type: string) => { + searchParams.set('view', type); + setSearchParams(searchParams); + }; + + const buildLinkParams = (): string => { + const params = createSearchParams({ + trace: traceId, + view: viewType, + } as any); + (datasources || []).forEach((ds: string) => { + params.append('datasources', ds); + }); + return params.toString(); + }; + + return ( + +
+
+ +
+ {newWindowLink && ( + + Open Full Page + + + )} + + changeViewType('timeline')} icon={}> + Timeline + + changeViewType('flame')} icon={}> + Flame Graph + + changeViewType('map')} icon={}> + Map + + changeViewType('summary')} icon={}> + Summary + + + }> + + +
+ {renderContent()} +
+ ); +}; + +export default TraceView; diff --git a/web/src/components/view/trace/components/EventView.tsx b/web/src/components/view/trace/components/EventView.tsx new file mode 100644 index 0000000..7cb74cb --- /dev/null +++ b/web/src/components/view/trace/components/EventView.tsx @@ -0,0 +1,40 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Timeline, Typography } from '@douyinfe/semi-ui'; +import { Span, Event } from '@src/types'; +import { get } from 'lodash-es'; +import moment from 'moment'; +import React from 'react'; +const { Text } = Typography; + +const EventView: React.FC<{ span: Span }> = (props) => { + const { span } = props; + const events = get(span, 'events', []); + return ( + { + return { + content: {moment(event.timestamp / 1000000).format('YYYY-MM-DD HH:mm:ss.SSS')}, + time: event.name, + }; + })} + /> + ); +}; + +export default EventView; diff --git a/web/src/components/view/trace/components/SpanHeader.tsx b/web/src/components/view/trace/components/SpanHeader.tsx new file mode 100644 index 0000000..0c27f77 --- /dev/null +++ b/web/src/components/view/trace/components/SpanHeader.tsx @@ -0,0 +1,79 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Card, Space, Typography } from '@douyinfe/semi-ui'; +import { IntegrationIcon } from '@src/components'; +import { FormatRepositoryInst, Span } from '@src/types'; +import moment from 'moment'; +import React from 'react'; +import SpanStatus from './SpanStatus'; + +const { Text } = Typography; + +const SpanHeader: React.FC<{ span: Span }> = (props) => { + const { span } = props; + const language = span.process.sdkLanguage; + const serviceName = span.process.serviceName; + return ( + + + {serviceName} + + {span.name} + + + } + description={ + + + Kind: + + + {span.kind} + + + Start Time: + + + {moment(span.startTime / 1000000).format('YYYY-MM-DD HH:mm:ss.SSS')} + + + Duration: + + + {FormatRepositoryInst.get('ns').formatString(span.duration, 3)} + + + Span ID: + + + {span.spanId} + + + + } + /> + ); +}; + +export default SpanHeader; diff --git a/web/src/components/view/trace/components/SpanStatus.tsx b/web/src/components/view/trace/components/SpanStatus.tsx new file mode 100644 index 0000000..dfbbedd --- /dev/null +++ b/web/src/components/view/trace/components/SpanStatus.tsx @@ -0,0 +1,41 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Tag } from '@douyinfe/semi-ui'; +import { Span } from '@src/types'; +import { get } from 'lodash-es'; +import React from 'react'; + +const SpanStatus: React.FC<{ span: Span; width?: string }> = (props) => { + const { span, width } = props; + const status = get(span, 'status.code'); + return ( + + {`${status}`} + + ); +}; + +export default SpanStatus; diff --git a/web/src/components/view/trace/components/SpanType.tsx b/web/src/components/view/trace/components/SpanType.tsx new file mode 100644 index 0000000..aebe3c3 --- /dev/null +++ b/web/src/components/view/trace/components/SpanType.tsx @@ -0,0 +1,45 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Tag } from '@douyinfe/semi-ui'; +import IntegrationIcon from '@src/components/common/IntegrationIcon'; +import { IconRepositoryInst, Span } from '@src/types'; +import { ColorKit, StringKit, TraceKit } from '@src/utils'; +import { isEmpty } from 'lodash-es'; +import React from 'react'; + +const SpanType: React.FC<{ span: Span }> = (props) => { + const { span } = props; + const spanType = TraceKit.getSpanType(span); + if (isEmpty(spanType)) { + return null; + } + const iconCls = IconRepositoryInst.getIconCls(spanType); + if (isEmpty(iconCls)) { + return ( + + {spanType} + + ); + } + return ; +}; + +export default SpanType; diff --git a/web/src/components/view/trace/components/SpanView.tsx b/web/src/components/view/trace/components/SpanView.tsx new file mode 100644 index 0000000..3b0dc81 --- /dev/null +++ b/web/src/components/view/trace/components/SpanView.tsx @@ -0,0 +1,137 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { SideSheet, Tag, Tabs, TabPane, Descriptions } from '@douyinfe/semi-ui'; +import React, { useEffect, useRef, useState } from 'react'; +import moment from 'moment'; +import { AnnotationType, Span } from '@src/types'; +import { get } from 'lodash-es'; +import { TraceKit } from '@src/utils'; +import SpanHeader from './SpanHeader'; +import { Resizable } from 're-resizable'; +import { TraceViewStore, AnnotationStore } from '@src/stores'; +import { observer } from 'mobx-react-lite'; +import { useSearchParams } from 'react-router-dom'; +import { DashboardView } from '@src/components'; +import { DateTimeFormat } from '@src/constants'; +import EventView from './EventView'; + +const Dashboard: React.FC<{ span: Span; resizeStop: boolean }> = (props) => { + const { span, resizeStop } = props; + const dashboardId = '77NWe4jVz'; + const timeRange = TraceKit.calcMetricQueryTimeRange(span); + + useEffect(() => { + AnnotationStore.setAnnotations([ + { + type: AnnotationType.Span, + timestamp: Math.floor(span.startTime / 1000000), + data: { + span: span, + }, + }, + ]); + }, [span]); + + useEffect(() => { + // NOTE: hack: tiger window resize event, then resize dashboard view + window.dispatchEvent(new Event('resize')); + }, [resizeStop]); + + return ( + + ); +}; + +const SpanView: React.FC<{ span?: Span }> = (props) => { + const { span } = props; + const initWidth = useRef(document.body.clientWidth * 0.65); + const [width, setWidth] = useState(initWidth.current); + const [finishResize, setFinishResize] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + if (!span) { + return null; + } + const events = get(span, 'events', []); + + return ( + } + size="large" + width={width} + height={'100vh'} + motion={false} + mask={false} + visible={TraceViewStore.viewVisibleSpanView} + onCancel={() => { + TraceViewStore.setVisibleSpanView(false); + }}> + { + initWidth.current += d.width; + setFinishResize(!finishResize); + }} + onResize={(_e, _direction, _ref, d) => { + setWidth(d.width + initWidth.current); + }}> + { + searchParams.set('tab', active); + setSearchParams(searchParams); + }}> + + + + + + + + + + + Events {events.length} + + } + itemKey="events"> + + + + + + ); +}; + +export default observer(SpanView); diff --git a/web/src/features/trace/flame/FlameView.tsx b/web/src/components/view/trace/flame/FlameView.tsx similarity index 96% rename from web/src/features/trace/flame/FlameView.tsx rename to web/src/components/view/trace/flame/FlameView.tsx index c7b4ad1..590d9c7 100644 --- a/web/src/features/trace/flame/FlameView.tsx +++ b/web/src/components/view/trace/flame/FlameView.tsx @@ -20,7 +20,7 @@ import { FlamegraphRenderer } from '@pyroscope/flamegraph'; import '@pyroscope/flamegraph/dist/index.css'; import { Trace } from '@src/types'; import React from 'react'; -import { default as TraceKit } from '../util/trace'; +import { TraceKit } from '@src/utils'; const FlameView: React.FC<{ traces: Trace[] }> = (props) => { const { traces } = props; diff --git a/web/src/features/trace/timeline/TimelineView.tsx b/web/src/components/view/trace/timeline/TimelineView.tsx similarity index 63% rename from web/src/features/trace/timeline/TimelineView.tsx rename to web/src/components/view/trace/timeline/TimelineView.tsx index 98684d7..c90fb76 100644 --- a/web/src/features/trace/timeline/TimelineView.tsx +++ b/web/src/components/view/trace/timeline/TimelineView.tsx @@ -17,35 +17,68 @@ under the License. */ import { Card, Space, Table, Typography } from '@douyinfe/semi-ui'; import { IntegrationIcon } from '@src/components'; -import { FormatRepositoryInst, Span, Trace } from '@src/types'; +import { TraceViewStore } from '@src/stores'; +import { FormatRepositoryInst, Span } from '@src/types'; import moment from 'moment'; import React, { useEffect, useState } from 'react'; +import SpanStatus from '../components/SpanStatus'; +import SpanType from '../components/SpanType'; import SpanView from '../components/SpanView'; -import { default as TraceKit } from '../util/trace'; const { Text } = Typography; -const TimelineView: React.FC<{ traces: Trace[] }> = (props) => { - const { traces } = props; - const [traceTree, setTraceTree] = useState([]); +const TimelineView: React.FC<{ + traceTree: Span[]; + spanId?: string; + spanMap?: Map; + openSelectedSpan?: boolean; +}> = (props) => { + const { traceTree, spanId, spanMap, openSelectedSpan } = props; const [currentSpan, setCurrentSpan] = useState(undefined); - const [visible, setVisible] = useState(false); - useEffect(() => { - setTraceTree(TraceKit.buildTraceTree(traces)); - }, [traces]); + if (openSelectedSpan && spanMap) { + const span = spanMap.get(spanId || ''); + if (span) { + TraceViewStore.setVisibleSpanView(true); + setCurrentSpan(span); + } + } + }, [spanId, spanMap, openSelectedSpan]); + + const onCell = (record: any) => { + if (record.spanId === spanId) { + return { + className: 'span-highlight', + }; + } else { + return {}; + } + }; return ( - - {visible && } + + { + return { + onClick: (_e) => { + setCurrentSpan(r); + TraceViewStore.setVisibleSpanView(true); + }, + style: { cursor: 'pointer' }, + }; + }} columns={[ { title: 'Service & Operation', @@ -56,19 +89,31 @@ const TimelineView: React.FC<{ traces: Trace[] }> = (props) => { return (
{ - setCurrentSpan(r); - setVisible(true); - }}> - }> + ref={(div) => { + if (r.spanId === spanId) { + div?.scrollIntoView({ behavior: 'auto' }); + } + }} + style={{ display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}> + }> {serviceName} {r.name} +
); }, + onCell: onCell, + }, + { + title: 'Status', + dataIndex: 'status', + width: 80, + align: 'center', + render: (_text: any, r: Span) => { + return ; + }, }, { title: 'Exec Time', @@ -111,7 +156,7 @@ const TimelineView: React.FC<{ traces: Trace[] }> = (props) => { dataIndex: 'startTime', width: 120, render: (text: any) => { - return moment(text / 1000).format('HH:mm:ss.SSS'); + return moment(text / 1000000).format('HH:mm:ss.SSS'); }, }, ]} diff --git a/web/src/features/trace/trace-view.scss b/web/src/components/view/trace/trace-view.scss similarity index 85% rename from web/src/features/trace/trace-view.scss rename to web/src/components/view/trace/trace-view.scss index 4d52e4f..4c8ae31 100644 --- a/web/src/features/trace/trace-view.scss +++ b/web/src/components/view/trace/trace-view.scss @@ -22,6 +22,11 @@ under the License. padding-bottom: 4px !important; } + .span-highlight { + border: 1px solid var(--semi-color-primary) !important; + box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 12%), 0 6px 16px 0 rgba(0, 0, 0, 8%), 0 9px 28px 8px rgba(0, 0, 0, 5%); + } + .duration-percent { position: relative; width: 100%; diff --git a/web/src/constants/dashboard.ts b/web/src/constants/dashboard.ts index afc9ef5..9e35efa 100644 --- a/web/src/constants/dashboard.ts +++ b/web/src/constants/dashboard.ts @@ -23,6 +23,8 @@ export const RowPanelType = 'row'; export const DateTimeFormat = 'YYYY-MM-DD HH:mm:ss'; export const DefaultColumns = 24; export const DefaultRowHeight = 30; +export const OneMinute = 60 * 1000; +export const OneHour = 60 * OneMinute; export const AutoRefreshList: QuickSelectItem[] = [ { value: '', diff --git a/web/src/contexts/PanelEditContextProvider.tsx b/web/src/contexts/PanelEditContextProvider.tsx index a813674..fe4b827 100644 --- a/web/src/contexts/PanelEditContextProvider.tsx +++ b/web/src/contexts/PanelEditContextProvider.tsx @@ -15,12 +15,13 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React, { createContext, MutableRefObject, useMemo, useRef, useState } from 'react'; +import React, { createContext, MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PanelSetting, Query, Tracker } from '@src/types'; import { ObjectKit } from '@src/utils'; -import { DashboardStore } from '@src/stores'; -import { cloneDeep, get } from 'lodash-es'; +import { DashboardStore, DatasourceStore } from '@src/stores'; +import { cloneDeep, get, isEmpty, unset } from 'lodash-es'; import { MixedDatasource } from '@src/constants'; +import { useSearchParams } from 'react-router-dom'; /* * Context for panel editor @@ -33,43 +34,118 @@ export const PanelEditContext = createContext({ /* * Context provider for each panel editor */ -export const PanelEditContextProvider: React.FC<{ initPanel: PanelSetting; children: React.ReactNode }> = (props) => { - const { initPanel = {}, children } = props; - const [panel, setPanel] = useState(initPanel); +export const PanelEditContextProvider: React.FC<{ + urlBind?: string; + initPanel: PanelSetting; + children: React.ReactNode; +}> = (props) => { + const { urlBind, initPanel = {}, children } = props; + const [searchParams, setSearchParams] = useSearchParams(); + + const getOptions = useCallback( + (key: string) => { + const defaultDS = DatasourceStore.getDefaultDatasource(); + const defaultPanelCfg = defaultDS?.plugin.getDefaultParams(); + const options = `${searchParams.get(key)}`; + if (!options || isEmpty(options)) { + return defaultPanelCfg; + } + try { + const panel = JSON.parse(options); + if (!panel) { + return defaultPanelCfg; + } + return panel; + } catch (err) { + console.warn('parse metric explore error', err); + } + return defaultPanelCfg; + }, + [searchParams] + ); + + const getInitPanel = useCallback(() => { + if (!isEmpty(urlBind)) { + return getOptions(`${urlBind}`); + } + return initPanel; + }, [urlBind, getOptions, initPanel]); + + const panelToString = (p: PanelSetting): string => { + return JSON.stringify(p, (_key: string, value: any) => { + if (isEmpty(value)) return undefined; + return value; + }); + }; + + const [panel, setPanel] = useState(null); const panelTracker = useRef() as MutableRefObject>; + const urlTracker = useRef() as MutableRefObject>; + useMemo(() => { - panelTracker.current = new Tracker(initPanel); - setPanel(initPanel); - }, [initPanel]); + const init = ObjectKit.cleanEmptyProperties(getInitPanel()); + panelTracker.current = new Tracker(init); + urlTracker.current = new Tracker(panelToString(init)); + setPanel(init); + }, [getInitPanel]); + + useEffect(() => { + if (!isEmpty(urlBind)) { + const panel = getOptions(`${urlBind}`); + setPanel(panel); + } + }, [urlBind, searchParams, getOptions]); /* * Modify panel options */ const modifyPanel = (cfg: PanelSetting, overwrite?: boolean) => { - const newPanel = cloneDeep(ObjectKit.merge(panel || {}, ObjectKit.removeUnderscoreProperties(cfg))); + let newPanel = ObjectKit.cleanEmptyProperties( + cloneDeep(ObjectKit.merge(panel || {}, ObjectKit.removeUnderscoreProperties(cfg))) + ); if (overwrite || panelTracker.current.isChanged(newPanel)) { - const panelDatasourceUID = get(newPanel, 'datasource.uid'); - if (panelDatasourceUID !== MixedDatasource) { - // if panel's datasource not mixed, need update all targets' datasource + const newPanelDatasource = get(newPanel, 'datasource'); + const newPanelDatasourceUID = get(newPanel, 'datasource.uid'); + const oldPanelDatasourceUID = get(panelTracker.current.getVal(), 'datasource.uid'); + + if ( + DatasourceStore.getDatasourceCategory(`${newPanelDatasourceUID}`) !== + DatasourceStore.getDatasourceCategory(`${oldPanelDatasourceUID}`) + ) { + const ds = DatasourceStore.getDatasource(newPanelDatasourceUID); + newPanel = ds?.plugin.getDefaultParams(); + newPanel.datasource = newPanelDatasource; + } + console.error('change panel data.......', newPanel); + + if (newPanelDatasourceUID !== MixedDatasource) { + // if panel's datasource not mixed, need clean all targets' datasource, use panel datasource (newPanel.targets || []).forEach((target: Query) => { - target.datasource = { uid: panelDatasourceUID, type: get(newPanel, 'datasource.type') }; + unset(target, 'datasource'); }); } - console.error('change panel data.......', newPanel); - panelTracker.current.setNewVal(newPanel); // NOTE: need modify dashboard's panel to trigger panel options modify event // because clone create new object, modify dashboard panel ref DashboardStore.updatePanel(newPanel); - setPanel(newPanel); + if (isEmpty(urlBind)) { + setPanel(newPanel); + } else { + const p = panelToString(newPanel); + if (urlTracker.current.isChanged(p)) { + urlTracker.current.setNewVal(p); + searchParams.set(`${urlBind}`, p); + setSearchParams(searchParams); + } + } } }; return ( {children} diff --git a/web/src/contexts/VariableContextProvider.tsx b/web/src/contexts/VariableContextProvider.tsx index 453f8a4..e5a5356 100644 --- a/web/src/contexts/VariableContextProvider.tsx +++ b/web/src/contexts/VariableContextProvider.tsx @@ -18,7 +18,7 @@ under the License. import React, { createContext, MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { SearchParamKeys, Tracker, Variable } from '@src/types'; -import { isEmpty, set } from 'lodash-es'; +import { get, has, isEmpty, set } from 'lodash-es'; export const VariableContext = createContext({ variables: {}, @@ -30,30 +30,36 @@ export const VariableContext = createContext({ refresh: () => {}, }); -const getValues = (variables: Variable[], searchParams: URLSearchParams): object => { +const getValues = (variables: Variable[], searchParams: URLSearchParams, initValues: any): object => { const newValues = {}; variables.forEach((variable: Variable) => { if (searchParams.has(variable.name)) { - // FIXME: add multi/all logic if (variable.multi) { set(newValues, variable.name, searchParams.getAll(variable.name)); } else { set(newValues, variable.name, searchParams.get(variable.name)); } + } else if (has(initValues, variable.name)) { + // FIXME: add multi/all logic + set(newValues, variable.name, get(initValues, variable.name)); } }); return newValues; }; -export const VariableContextProvider: React.FC<{ variables: Variable[]; children: React.ReactNode }> = (props) => { - const { children, variables } = props; +export const VariableContextProvider: React.FC<{ + variables: Variable[]; + children: React.ReactNode; + initValues?: object; +}> = (props) => { + const { children, variables, initValues } = props; const [searchParams] = useSearchParams(); const [valuesOfVariable, setValuesOfVariable] = useState(() => { - return getValues(variables, searchParams); + return getValues(variables, searchParams, initValues); }); const [refreshTime, setRefreshTime] = useState(0); - const from = searchParams.get(SearchParamKeys.From) || ''; - const to = searchParams.get(SearchParamKeys.To) || ''; + const from = searchParams.get(SearchParamKeys.From) || get(initValues, 'from', ''); + const to = searchParams.get(SearchParamKeys.To) || get(initValues, 'to', ''); const refreshInterval = searchParams.get(SearchParamKeys.Refresh) || ''; const valuesTrackerRef = useRef() as MutableRefObject>; @@ -65,12 +71,12 @@ export const VariableContextProvider: React.FC<{ variables: Variable[]; children if (isEmpty(variables)) { return; } - const newValues = getValues(variables, searchParams); + const newValues = getValues(variables, searchParams, initValues); if (valuesTrackerRef.current.isChanged(newValues)) { valuesTrackerRef.current.setNewVal(newValues); setValuesOfVariable(newValues); } - }, [searchParams, variables]); + }, [searchParams, variables, initValues]); const refresh = () => { setRefreshTime(refreshTime + 1); diff --git a/web/src/features/chart/ChartDetail.tsx b/web/src/features/chart/ChartDetail.tsx index 65130b7..7a962d8 100644 --- a/web/src/features/chart/ChartDetail.tsx +++ b/web/src/features/chart/ChartDetail.tsx @@ -17,15 +17,16 @@ under the License. */ import { Button, SideSheet, Typography } from '@douyinfe/semi-ui'; import { IconClose, IconSaveStroked } from '@douyinfe/semi-icons'; -import { DatasourceSelectForm, Icon, IntegrationIcon, MetricExplore, Notification } from '@src/components'; +import { DatasourceSelectForm, Icon, IntegrationIcon, DataExplore, Notification } from '@src/components'; import { PanelEditContext, PanelEditContextProvider } from '@src/contexts'; import { ChartSrv } from '@src/services'; import { DatasourceStore } from '@src/stores'; -import { Chart, DatasourceInstance, PanelSetting } from '@src/types'; +import { Chart, DatasourceCategory, DatasourceInstance, PanelSetting } from '@src/types'; import { ApiKit } from '@src/utils'; -import React, { useContext, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; import { createSearchParams, useNavigate } from 'react-router-dom'; import { get, unset } from 'lodash-es'; +import { Resizable } from 're-resizable'; const { Text, Title } = Typography; const ChartDetail: React.FC<{ chart: Chart; setVisible: (v: boolean) => void }> = (props) => { @@ -52,6 +53,7 @@ const ChartDetail: React.FC<{ chart: Chart; setVisible: (v: boolean) => void }> { modifyPanel({ datasource: { uid: instance.setting.uid } }); @@ -90,7 +92,7 @@ const ChartDetail: React.FC<{ chart: Chart; setVisible: (v: boolean) => void }> - { - const p = cloneDeep(panel); - p.title = 'Panel title'; - return [ - { - model: p, - }, - ]; - }} - /> - - + {DatasourceKit.isMetric(datasource) && ( +
+ + + { + const p = cloneDeep(panel); + p.title = 'Panel title'; + return [ + { + model: p, + }, + ]; + }} + /> + +
+ )} {renderContent()} @@ -128,36 +98,8 @@ const ExploreContent: React.FC = () => { }; const Explore: React.FC = () => { - const [searchParams] = useSearchParams(); - - const getOptions = (key: string) => { - const options = `${searchParams.get(key)}`; - if (!options || isEmpty(options)) { - return DefaultPanel; - } - try { - const panel = JSON.parse(options); - if (!panel) { - return DefaultPanel; - } - if (isEmpty(get(panel, 'targets', []))) { - set(panel, 'targets', [{}]); - } - set(panel, 'type', 'timeseries'); - set(panel, 'fieldConfig', { - defaults: { - unit: 'short', - }, - }); - return panel; - } catch (err) { - console.warn('parse metric explore error', err); - } - return DefaultPanel; - }; - return ( - + ); diff --git a/web/src/features/explore/module.ts b/web/src/features/explore/module.ts index 2a46f71..8f601d4 100644 --- a/web/src/features/explore/module.ts +++ b/web/src/features/explore/module.ts @@ -17,5 +17,15 @@ under the License. */ import { Feature, FeatureRepositoryInst } from '@src/types'; import Explore from '@src/features/explore/Explore'; +import { DatasourceStore } from '@src/stores'; +import { DatasourceKit } from '@src/utils'; -FeatureRepositoryInst.register(new Feature('/explore', 'Explore', 'Explore all data', Explore)); +FeatureRepositoryInst.register( + new Feature('/explore', 'Explore', 'Explore all data', Explore, (): string => { + const defaultDS = DatasourceStore.getDefaultDatasource(); + if (!defaultDS) { + return ''; + } + return DatasourceKit.getDatasourceDefaultParams(defaultDS.setting.uid); + }) +); diff --git a/web/src/features/setting/datasource/EditDataSource.tsx b/web/src/features/setting/datasource/EditDataSource.tsx index 16d2d65..9f803e8 100644 --- a/web/src/features/setting/datasource/EditDataSource.tsx +++ b/web/src/features/setting/datasource/EditDataSource.tsx @@ -20,10 +20,10 @@ import { isEmpty, get, filter } from 'lodash-es'; import { Button, Select, Card, Typography, Form, Space, Tag } from '@douyinfe/semi-ui'; import { IconSaveStroked } from '@douyinfe/semi-icons'; import { DatasourceSrv } from '@src/services'; -import { DatasourcePlugin, DatasourceRepositoryInst, DatasourceSetting } from '@src/types'; +import { DatasourceCategory, DatasourcePlugin, DatasourceRepositoryInst, DatasourceSetting } from '@src/types'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { Icon, Notification } from '@src/components'; -import { ApiKit, ObjectKit, TimeKit } from '@src/utils'; +import { DatasourceSelect, Icon, Notification } from '@src/components'; +import { ApiKit, DatasourceKit, ObjectKit, TimeKit } from '@src/utils'; import { useRequest } from '@src/hooks'; import { DatasourceStore } from '@src/stores'; import DeleteDatasourceButton from './components/DeleteDatasourceButton'; @@ -120,9 +120,11 @@ const EditDataSource: React.FC = () => { } return null; }; + const gotoDatasourceList = () => { navigate({ pathname: '/setting/datasources' }); }; + return ( { - + + + - {uid && ( - { - navigate({ - pathname: '/setting/datasources', - }); - }} - text="Delete" - /> + <> + + { + navigate({ + pathname: '/setting/datasources', + }); + }} + text="Delete" + /> + )} diff --git a/web/src/features/trace/TracePage.tsx b/web/src/features/trace/TracePage.tsx new file mode 100644 index 0000000..032a77c --- /dev/null +++ b/web/src/features/trace/TracePage.tsx @@ -0,0 +1,34 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { TraceView } from '@src/components'; +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; + +const TracePage: React.FC = () => { + const [searchParams] = useSearchParams(); + return ( + + ); +}; + +export default TracePage; diff --git a/web/src/features/trace/TraceView.tsx b/web/src/features/trace/TraceView.tsx deleted file mode 100644 index d7bf2d6..0000000 --- a/web/src/features/trace/TraceView.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* -Licensed to LinDB under one or more contributor -license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright -ownership. LinDB licenses this file to you under -the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -*/ -import { Button, Dropdown, Layout } from '@douyinfe/semi-ui'; -import { IconInheritStroked, IconDescend, IconFlowChartStroked, IconPieChart2Stroked } from '@douyinfe/semi-icons'; -import { useTrace } from '@src/hooks'; -import React, { useState } from 'react'; -import TimelineView from './timeline/TimelineView'; -import { Trace } from '@src/types'; -import FlameView from './flame/FlameView'; -import { cloneDeep } from 'lodash-es'; -import { Loading } from '@src/components'; -import './trace-view.scss'; -import { useSearchParams } from 'react-router-dom'; - -const { Header } = Layout; -const TraceView: React.FC = () => { - const [viewType, setViewType] = useState('timeline'); - const [searchParams] = useSearchParams(); - const { loading, result } = useTrace({ - datasource: { uid: 'dqOPrJ6Vk' }, - request: { - traceId: `${searchParams.get('traceId')}`, - }, - }); - if (loading) { - return ( -
- -
- ); - } - console.error(result); - if (!result) { - return null; - } - const renderContent = () => { - switch (viewType) { - case 'flame': - return ; - default: - return ; - } - }; - - const renderIcon = () => { - switch (viewType) { - case 'flame': - return ; - default: - return ; - } - }; - - return ( - -
-
adddd
- - setViewType('timeline')} icon={}> - Timeline - - setViewType('flame')} icon={}> - Flame Graph - - setViewType('map')} icon={}> - Map - - setViewType('summary')} icon={}> - Summary - - - }> - - -
- {renderContent()} -
- ); -}; - -export default TraceView; diff --git a/web/src/features/trace/components/SpanView.tsx b/web/src/features/trace/components/SpanView.tsx deleted file mode 100644 index b6af881..0000000 --- a/web/src/features/trace/components/SpanView.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* -Licensed to LinDB under one or more contributor -license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright -ownership. LinDB licenses this file to you under -the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -*/ -import { Card, Typography, SideSheet, Space, Tag, Tabs, TabPane, Descriptions, List, Divider } from '@douyinfe/semi-ui'; -import React from 'react'; -import moment from 'moment'; -import { Span } from '@src/types'; -import { IntegrationIcon } from '@src/components'; -import { get } from 'lodash-es'; -import { default as TraceKit } from '../util/trace'; - -const { Text, Title } = Typography; - -const SpanView: React.FC<{ visible: boolean; setVisible: (v: boolean) => void; span?: Span }> = (props) => { - const { visible, setVisible, span } = props; - if (!span) { - return null; - } - const events = get(span, 'events', []); - const renderTitle = () => { - const status = 'ok'; - - return ( -
- - - <Space> - <IntegrationIcon integration={span.process.sdkLanguage} /> - <Title heading={2}>{span.process.serviceName} - - {span.name} - - - - - } - description={ - - Start Time: - {moment(span.startTime / 1000).format('YYYY-MM-DD HH:mm:ss.SSS')} - Duration: - {span.duration / 1000} ms - Span ID: - {span.spanId} - - {status === 'ERROR' ? 'ERROR' : 'OK'} - - - } - /> -
- ); - }; - - return ( - setVisible(false)}> - - - - - - - - - Events {events.length} - - } - itemKey="3"> - ( - - - {[ - - {moment(item.timestamp / 1000000).format('YYYY-MM-DD HH:mm:ss.SSS')} - , - ]} - - - )} - /> - - - - ); -}; - -export default SpanView; diff --git a/web/src/hooks/use.trace.ts b/web/src/hooks/use.trace.ts index efb87b9..7c316bc 100644 --- a/web/src/hooks/use.trace.ts +++ b/web/src/hooks/use.trace.ts @@ -19,42 +19,48 @@ import { MixedDatasource } from '@src/constants'; import { VariableContext } from '@src/contexts'; import { DataQuerySrv } from '@src/services'; import { DatasourceStore } from '@src/stores'; -import { DataQuery, Query, TimeRange } from '@src/types'; -import { TimeKit } from '@src/utils'; +import { DataQuery, Query, Span, TimeRange, Trace } from '@src/types'; +import { StringKit, TimeKit, TraceKit } from '@src/utils'; import { isEmpty, cloneDeep, get } from 'lodash-es'; -import { useContext } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useRequest } from './use.request'; -const getDataQuery = (query: Query, variables: object): DataQuery => { +const getDataQuery = (queries: Query[], variables: object): DataQuery => { const dataQuery: DataQuery = { queries: [] }; - let datasourceUID = get(query.datasource, 'uid'); - if (datasourceUID === MixedDatasource) { - // ignore mixed datasource - return dataQuery; - } - const ds = DatasourceStore.getDatasource(datasourceUID); - if (!ds) { - return dataQuery; - } - // NOTE: need clone new q, because rewrite query will modify it - const queryAfterRewrite = ds.api.rewriteQuery(cloneDeep(query), variables); - if (isEmpty(queryAfterRewrite)) { - return dataQuery; - } - // need set datasource, maybe target no datasource setting, using default datasource - queryAfterRewrite.datasource = { uid: datasourceUID }; - queryAfterRewrite.refId = 'A'; - // add query request into batch - dataQuery.queries.push(queryAfterRewrite); + (queries || []).forEach((query: Query, idx: number) => { + let datasourceUID = get(query.datasource, 'uid'); + if (datasourceUID === MixedDatasource) { + // ignore mixed datasource + return dataQuery; + } + const ds = DatasourceStore.getDatasource(datasourceUID); + if (!ds) { + return dataQuery; + } + // NOTE: need clone new q, because rewrite query will modify it + const queryAfterRewrite = ds.api.rewriteQuery(cloneDeep(query), variables); + if (isEmpty(queryAfterRewrite)) { + return dataQuery; + } + // need set datasource, maybe target no datasource setting, using default datasource + queryAfterRewrite.datasource = { uid: datasourceUID }; + queryAfterRewrite.refId = StringKit.generateCharSeq(idx); + // add query request into batch + dataQuery.queries.push(queryAfterRewrite); + }); return dataQuery; }; -export const useTrace = (queries: Query) => { +export const useTrace = (queries: Query[]) => { const { variables, from, to, refreshTime } = useContext(VariableContext); const dataQuery: DataQuery = getDataQuery(queries, variables); + const [traces, setTraces] = useState([]); + const [tree, setTree] = useState([]); + const [spanMap, setSpanMap] = useState>(); const { result, loading, refetch, error } = useRequest( ['get_trace_data', dataQuery, from, to, refreshTime], // watch dataQuery/from/to if changed async () => { + console.error('trace request.....'); const range: TimeRange = {}; if (!isEmpty(from)) { range.from = TimeKit.parseTime(from); @@ -67,9 +73,20 @@ export const useTrace = (queries: Query) => { }, { enabled: !isEmpty(dataQuery.queries) } ); + + useEffect(() => { + const traces = TraceKit.createTraceDatasets(result); + setTraces(traces); + const tree = TraceKit.buildTraceTree(traces); + setTree(tree.tree); + setSpanMap(tree.spanMap); + }, [result]); + return { loading, - result: get(result, 'A', []), + traces, + tree, + spanMap, error, refetch, }; diff --git a/web/src/plugins/datasources/lindb/SettingEditor.tsx b/web/src/plugins/datasources/lindb/SettingEditor.tsx index f783dce..b80764c 100644 --- a/web/src/plugins/datasources/lindb/SettingEditor.tsx +++ b/web/src/plugins/datasources/lindb/SettingEditor.tsx @@ -37,6 +37,7 @@ export const SettingEditor: React.FC = () => { + ); diff --git a/web/src/plugins/datasources/lindb/module.ts b/web/src/plugins/datasources/lindb/module.ts index 9909193..7e8fd22 100644 --- a/web/src/plugins/datasources/lindb/module.ts +++ b/web/src/plugins/datasources/lindb/module.ts @@ -32,6 +32,25 @@ const LinDB = new DatasourcePlugin( ); LinDB.setSettingEditor(SettingEditor) + .setDefaultParams({ + targets: [ + { + refId: 'A', + request: { + fields: [], + groupBy: [], + metric: '', + where: [], + }, + }, + ], + type: 'timeseries', + fieldConfig: { + defaults: { + unit: 'short', + }, + }, + }) .setQueryEditor(QueryEditor) .setVariableEditor(VariableEditor) .setDarkLogo(DarkLogo) diff --git a/web/src/plugins/datasources/lingo/QueryEditor.tsx b/web/src/plugins/datasources/lingo/QueryEditor.tsx new file mode 100644 index 0000000..d2d2ac4 --- /dev/null +++ b/web/src/plugins/datasources/lingo/QueryEditor.tsx @@ -0,0 +1,66 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Form } from '@douyinfe/semi-ui'; +import { QueryEditContext } from '@src/contexts'; +import { Query, QueryEditorProps, Tracker } from '@src/types'; +import { get } from 'lodash-es'; +import React, { MutableRefObject, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import './query-edit.scss'; + +const QueryEditor: React.FC = (props) => { + const { datasource } = props; + const { target, modifyTarget } = useContext(QueryEditContext); + const requestTracker = useRef() as MutableRefObject>; + const formApi = useRef() as MutableRefObject; + + const getInitRequest = useCallback(() => { + return get(target, 'request', {}); + }, [target]); + + useMemo(() => { + requestTracker.current = new Tracker(getInitRequest()); + }, [getInitRequest]); + + useEffect(() => { + formApi.current.setValues(getInitRequest()); + }, [datasource, getInitRequest]); + + return ( + <> +
{ + formApi.current = api; + }} + className="lingo-query-editor" + layout="horizontal" + labelPosition="left" + onSubmit={(values: any) => { + if (!requestTracker.current.isChanged(values)) { + return; + } + requestTracker.current.setNewVal(values); + // change query edit context's values + modifyTarget({ request: values } as Query); + }}> + + + + ); +}; + +export default QueryEditor; diff --git a/web/src/plugins/datasources/lingo/module.ts b/web/src/plugins/datasources/lingo/module.ts index 0391213..1462aaa 100644 --- a/web/src/plugins/datasources/lingo/module.ts +++ b/web/src/plugins/datasources/lingo/module.ts @@ -18,6 +18,7 @@ under the License. import { DatasourceCategory, DatasourcePlugin, DatasourceRepositoryInst } from '@src/types'; import { LinGoDatasource } from './Datasource'; import Logo from './images/logo.svg'; +import QueryEditor from './QueryEditor'; import { SettingEditor } from './SettingEditor'; const LinGo = new DatasourcePlugin( @@ -28,5 +29,18 @@ const LinGo = new DatasourcePlugin( LinGoDatasource ); -LinGo.setSettingEditor(SettingEditor).setDarkLogo(Logo).setLightLogo(Logo); +LinGo.setQueryEditor(QueryEditor) + .setDefaultParams({ + targets: [ + { + refId: 'A', + request: { + traceId: '', + }, + }, + ], + }) + .setSettingEditor(SettingEditor) + .setDarkLogo(Logo) + .setLightLogo(Logo); DatasourceRepositoryInst.register(LinGo); diff --git a/web/src/plugins/datasources/lingo/query-edit.scss b/web/src/plugins/datasources/lingo/query-edit.scss new file mode 100644 index 0000000..554c1d2 --- /dev/null +++ b/web/src/plugins/datasources/lingo/query-edit.scss @@ -0,0 +1,20 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +.lingo-query-editor { + gap: 4px; +} diff --git a/web/src/plugins/visualizations/timeseries/TimeSeries.tsx b/web/src/plugins/visualizations/timeseries/TimeSeries.tsx index 4d3ecbb..3ba0996 100644 --- a/web/src/plugins/visualizations/timeseries/TimeSeries.tsx +++ b/web/src/plugins/visualizations/timeseries/TimeSeries.tsx @@ -19,11 +19,17 @@ import React from 'react'; import { VisualizationProps } from '@src/types'; import { TimeSeriesChart } from './components/TimeSeriesChart'; import './components/timeseries.scss'; +import { ExemplarView } from '@src/components'; /** * TimeSeries is a visualization component for time series chart. */ export const TimeSeries: React.FC = (props) => { const { panel, theme, datasets } = props; - return ; + return ( + <> + + ; + + ); }; diff --git a/web/src/plugins/visualizations/timeseries/components/Menu.tsx b/web/src/plugins/visualizations/timeseries/components/Menu.tsx index 7f42809..9829134 100644 --- a/web/src/plugins/visualizations/timeseries/components/Menu.tsx +++ b/web/src/plugins/visualizations/timeseries/components/Menu.tsx @@ -15,16 +15,15 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Popover, List, Typography } from '@douyinfe/semi-ui'; +import { Dropdown } from '@douyinfe/semi-ui'; import { Icon } from '@src/components'; -import { PlatformStore } from '@src/stores'; +import { DatasourceStore, ExemplarStore, PlatformStore } from '@src/stores'; import { MouseEvent, MouseEventType } from '@src/types'; import { Chart } from 'chart.js'; import { reaction } from 'mobx'; import React, { MutableRefObject, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import * as _ from 'lodash-es'; -const { Text } = Typography; +import { get, isEmpty } from 'lodash-es'; const Menu: React.FC<{ chart: Chart | null }> = (props) => { const { chart } = props; @@ -33,6 +32,7 @@ const Menu: React.FC<{ chart: Chart | null }> = (props) => { const left = useRef(0); const timer = useRef(); const container = useRef() as MutableRefObject; + const event = useRef(); const [visible, setVisible] = useState(false); @@ -56,7 +56,7 @@ const Menu: React.FC<{ chart: Chart | null }> = (props) => { if (!chart || !native) { return; } - if (_.get(chartOfMove, 'id', 0) != _.get(chart, 'id', 0)) { + if (get(chartOfMove, 'id', 0) != get(chart, 'id', 0)) { return; } // disable click @@ -66,6 +66,7 @@ const Menu: React.FC<{ chart: Chart | null }> = (props) => { if (type === MouseEventType.Click) { chartRect.current = chart?.canvas?.getBoundingClientRect(); left.current = native.offsetX; + event.current = mouseEvent; setVisible(true); } else { removeMenu(); @@ -76,6 +77,15 @@ const Menu: React.FC<{ chart: Chart | null }> = (props) => { return () => disposer(); }, [chart]); + const isSupport = (key: string): boolean => { + const datasourceUID = get(event.current, 'series.meta.target.datasource.uid', ''); + const datasource = DatasourceStore.getDatasource(datasourceUID); + if (!datasource) { + return false; + } + return !isEmpty(get(datasource, `setting.config.${key}`)); + }; + const keepMenu = () => { if (!timer.current) { return; @@ -84,41 +94,63 @@ const Menu: React.FC<{ chart: Chart | null }> = (props) => { timer.current = null; }; - if (!visible) { + if (!visible || !chartRect.current) { return null; } return createPortal(
- ( - - }> {item.name} - - )} - /> - } + menu={[ + { + node: 'item', + name: 'View related traces', + icon: , + disabled: !isSupport('traceDatasources'), + onClick: () => { + const series = get(event.current, 'series'); + if (series) { + ExemplarStore.setSelectDataPoint({ + timestamp: get(event.current, 'timestamp', 0), + point: series, + }); + } else { + ExemplarStore.setSelectDataPoint(null); + } + setVisible(false); + }, + }, + { + node: 'item', + name: 'View related logs', + icon: , + disabled: !isSupport('loggingDatasources'), + }, + { + node: 'item', + name: 'View related incidents', + icon: , + + disabled: !isSupport('alertingDatasources'), + }, + { + node: 'item', + name: 'Find correlated metric', + icon: , + disabled: !isSupport('metricDatasources'), + }, + ]} />
, document.body diff --git a/web/src/plugins/visualizations/timeseries/components/TimeSeriesChart.tsx b/web/src/plugins/visualizations/timeseries/components/TimeSeriesChart.tsx index 50d6175..b150a64 100644 --- a/web/src/plugins/visualizations/timeseries/components/TimeSeriesChart.tsx +++ b/web/src/plugins/visualizations/timeseries/components/TimeSeriesChart.tsx @@ -15,14 +15,20 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import React, { MutableRefObject, useEffect, useRef, useState } from 'react'; +import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react'; import { Legend } from '@src/plugins/visualizations/timeseries/components/Legend'; import Tooltip from '@src/plugins/visualizations/timeseries/components/Tooltip'; -import { getChartConfig, modifyChartConfigs } from '@src/plugins/visualizations/timeseries/components/chart.config'; +import { + getChartConfig, + modifyChartConfigs, + transferAnnotations, +} from '@src/plugins/visualizations/timeseries/components/chart.config'; import { Chart, registerables } from 'chart.js'; -import { cloneDeep, get, set, find, isEmpty } from 'lodash-es'; +import { cloneDeep, get, set, find, isEmpty, startsWith, remove } from 'lodash-es'; import classNames from 'classnames'; import { + Annotation, + AnnotationPrefix, FormatRepositoryInst, LegendPlacement, MouseEventType, @@ -30,13 +36,15 @@ import { SearchParamKeys, ThemeType, } from '@src/types'; -import { PlatformStore } from '@src/stores'; +import { AnnotationStore, PlatformStore } from '@src/stores'; import { CSSKit } from '@src/utils'; import annotationPlugin from 'chartjs-plugin-annotation'; import { getCustomOptions } from '../types'; import moment from 'moment'; import { DateTimeFormat } from '@src/constants'; import { useSearchParams } from 'react-router-dom'; +import Menu from './Menu'; +import { reaction, toJS } from 'mobx'; Chart.register(annotationPlugin); Chart.register(...registerables); @@ -62,6 +70,42 @@ export const TimeSeriesChart: React.FC<{ datasets: any; theme: ThemeType; panel: const currPointIndex = useRef(-1); + const setAnnotations = useCallback( + (chartCfg: any, annotations: Annotation[]): boolean => { + const chartAnnotations: any = get(chartCfg, 'options.plugins.annotation.annotations'); + // remove old annotations + remove(chartAnnotations, (a: any) => { + return startsWith(a.id, AnnotationPrefix); + }); + const newAnnotations = transferAnnotations( + annotations, + theme, + get(datasets, 'times', []), + get(datasets, 'interval', 0) + ); + if (!isEmpty(newAnnotations)) { + chartAnnotations.push(...newAnnotations); + return true; + } + return false; + }, + [datasets, theme] + ); + + useEffect(() => { + const disposer = reaction( + () => toJS(AnnotationStore.annotations), + (annotations: Annotation[]) => { + const currChart = chartInstance.current; + if (setAnnotations(currChart, annotations)) { + currChart?.update(); + } + } + ); + + return () => disposer(); + }, [theme, setAnnotations]); + const isZoom = (chart: Chart | null) => { return get(chart, 'options.zoom', false); }; @@ -147,10 +191,22 @@ export const TimeSeriesChart: React.FC<{ datasets: any; theme: ThemeType; panel: }; const handleMouseClick = (e: MouseEvent) => { - console.log('xxxxxx..... cccccc'); + if (!chartInstance.current) { + return; + } + const chart = chartInstance.current; + const points: any = chart.getElementsAtEventForMode(e, 'point', { intersect: true }, false); + if (!points || points.length <= 0) { + return; + } + + const series = datasets.datasets[points[0].datasetIndex]; + const timestamp = datasets.times[points[0].index]; PlatformStore.setMouseEvent({ type: MouseEventType.Click, native: e, + series: series, + timestamp: timestamp, }); }; @@ -269,8 +325,8 @@ export const TimeSeriesChart: React.FC<{ datasets: any; theme: ThemeType; panel: } const customOptions = getCustomOptions(panel); const chartCfg: any = getChartConfig(theme); + setAnnotations(chartCfg, AnnotationStore.annotations); chartCfg.options.legend = get(panel, 'options.legend', {}); - console.error('chart cfg', chartCfg, customOptions); if (!chartInstance.current) { chartCfg.data = datasets || []; modifyChartConfigs(chartCfg, customOptions); @@ -310,7 +366,7 @@ export const TimeSeriesChart: React.FC<{ datasets: any; theme: ThemeType; panel: } }); setSelectedSeries(Array.from(currentSelectedSet)); - console.error('re-render time series chart'); + console.error('re-render time series chart', datasets, chartInstance.current); }, [datasets, theme, panel]); /** @@ -346,6 +402,7 @@ export const TimeSeriesChart: React.FC<{ datasets: any; theme: ThemeType; panel: + ); }; diff --git a/web/src/plugins/visualizations/timeseries/components/chart.config.ts b/web/src/plugins/visualizations/timeseries/components/chart.config.ts index 4fd47c8..1ba0dd5 100644 --- a/web/src/plugins/visualizations/timeseries/components/chart.config.ts +++ b/web/src/plugins/visualizations/timeseries/components/chart.config.ts @@ -15,9 +15,9 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { ThemeType } from '@src/types'; +import { Annotation, AnnotationPrefix, AnnotationType, ThemeType } from '@src/types'; import { ColorKit, CSSKit } from '@src/utils'; -import { has, get, set, forIn } from 'lodash-es'; +import { has, get, set, forIn, indexOf } from 'lodash-es'; import { TimeSeriesOptions } from '../types'; export const format = (chart: any, val: number) => { @@ -108,6 +108,59 @@ export const modifyChartConfigs = (chart: any, cfg: TimeSeriesOptions) => { } }; +export const transferAnnotations = ( + annotations: Annotation[], + theme: ThemeType, + times: any[], + interval: number +): any[] => { + const rs: any[] = []; + (annotations || []).forEach((a: Annotation) => { + if (a.type === AnnotationType.Span) { + const spanId = get(a.data, 'span.spanId', ''); + const ts = a.timestamp - (a.timestamp % interval); + const idx = indexOf(times, ts); + if (idx < 0) { + return; + } + rs.push({ + id: `${AnnotationPrefix}${spanId}-point`, + type: 'point', + backgroundColor: CSSKit.getColor('--semi-color-primary', theme), + borderWidth: 0, + pointStyle: 'triangle', + radius: 6, + value: idx, + scaleID: 'x', + yAdjust: 6, + yValue: 0, + yScaleID: 'y', + }); + rs.push({ + id: `${AnnotationPrefix}${spanId}-line`, + type: 'line', + label: { + display: true, + content: 'Span', + opacity: 0, + color: CSSKit.getColor('--semi-color-text-2', theme), + backgroundColor: 'transparent', + font: { + size: 10, + weight: 400, + }, + }, + value: idx, + scaleID: 'x', + borderDash: [3, 1], + borderColor: CSSKit.getColor('--semi-color-primary', theme), + borderWidth: 1, + }); + } + }); + return rs; +}; + export const getChartConfig = (theme: ThemeType) => { return { type: 'line', @@ -175,6 +228,10 @@ export const getChartConfig = (theme: ThemeType) => { }, plugins: { annotation: { + clip: false, + common: { + // drawTime: 'afterDraw', + }, annotations: [], }, legend: { diff --git a/web/src/services/index.ts b/web/src/services/index.ts index 9af5874..90ca749 100644 --- a/web/src/services/index.ts +++ b/web/src/services/index.ts @@ -21,7 +21,6 @@ export { default as ComponentSrv } from './component.service'; export { default as TagSrv } from './tag.service'; export { default as OrgSrv } from './org.service'; export { default as PlatformSrv } from './platform.service'; -export { default as MetricSrv } from './metric.service'; export { default as AlertSrv } from './alert.service'; export { default as DashboardSrv } from './dashboard.service'; export { default as ChartSrv } from './chart.service'; diff --git a/web/src/services/metric.service.ts b/web/src/services/metric.service.ts deleted file mode 100644 index 753ab35..0000000 --- a/web/src/services/metric.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* -Licensed to LinDB under one or more contributor -license agreements. See the NOTICE file distributed with -this work for additional information regarding copyright -ownership. LinDB licenses this file to you under -the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. See the License for the -specific language governing permissions and limitations -under the License. -*/ -function getStatsList() { - return [ - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - { tags: { service: 'order.service' }, values: { count: 1000, latency: 1200 } }, - ]; -} - -export default { - getStatsList, -}; diff --git a/web/src/stores/annotation.store.ts b/web/src/stores/annotation.store.ts new file mode 100644 index 0000000..e7eb21e --- /dev/null +++ b/web/src/stores/annotation.store.ts @@ -0,0 +1,36 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { Annotation } from '@src/types'; +import { makeAutoObservable } from 'mobx'; + +class AnnotationStore { + annotations: Annotation[] = []; + constructor() { + makeAutoObservable(this); + } + + public addAnnotation(annotation: Annotation) { + this.annotations.push(annotation); + } + + public setAnnotations(annotations: Annotation[]) { + this.annotations = annotations; + } +} + +export default new AnnotationStore(); diff --git a/web/src/stores/datasource.store.ts b/web/src/stores/datasource.store.ts index cf51eaf..e626eb9 100644 --- a/web/src/stores/datasource.store.ts +++ b/web/src/stores/datasource.store.ts @@ -16,7 +16,7 @@ specific language governing permissions and limitations under the License. */ import { makeAutoObservable, toJS } from 'mobx'; -import { DatasourceInstance, DatasourceRepositoryInst, DatasourceSetting } from '@src/types'; +import { DatasourceCategory, DatasourceInstance, DatasourceRepositoryInst, DatasourceSetting } from '@src/types'; import { filter, find, get } from 'lodash-es'; import { DatasourceSrv } from '@src/services'; import { MixedDatasource } from '@src/constants'; @@ -43,6 +43,14 @@ class DatasourceStore { return null; } + getDatasourceCategory(uid: string): DatasourceCategory { + const ds = this.getDatasource(uid); + if (!ds) { + return DatasourceCategory.Unknown; + } + return ds.plugin.category; + } + getDatasources(includeMixed?: boolean): DatasourceInstance[] { return filter(this.datasources, (d: DatasourceInstance) => { if (includeMixed) { @@ -70,6 +78,11 @@ class DatasourceStore { name: MixedDatasource, type: 'mixed', }, + plugin: { + Name: MixedDatasource, + Type: 'mixed', + category: DatasourceCategory.Metric, + }, } as any); this.datasources = rs; } diff --git a/web/src/stores/exemplar.store.ts b/web/src/stores/exemplar.store.ts new file mode 100644 index 0000000..75bd1a7 --- /dev/null +++ b/web/src/stores/exemplar.store.ts @@ -0,0 +1,32 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { makeAutoObservable } from 'mobx'; + +class ExemplarStore { + selectDataPoint: any = null; + + constructor() { + makeAutoObservable(this); + } + + setSelectDataPoint(dataPoint: { timestamp: number; point: any } | null) { + this.selectDataPoint = dataPoint; + } +} + +export default new ExemplarStore(); diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts index 90ce091..cb3c58b 100644 --- a/web/src/stores/index.ts +++ b/web/src/stores/index.ts @@ -20,4 +20,7 @@ export { default as UserStore } from '@src/stores/user.store'; export { default as MenuStore } from '@src/stores/menu.store'; export { default as DatasourceStore } from '@src/stores/datasource.store'; export { default as ChartPendingAddStore } from '@src/stores/chart.pending.store'; +export { default as TraceViewStore } from '@src/stores/trace.view.store'; +export { default as ExemplarStore } from '@src/stores/exemplar.store'; export { default as DashboardStore } from '@src/stores/dashboard.store'; +export { default as AnnotationStore } from '@src/stores/annotation.store'; diff --git a/web/src/stores/trace.view.store.ts b/web/src/stores/trace.view.store.ts new file mode 100644 index 0000000..ffbe3f3 --- /dev/null +++ b/web/src/stores/trace.view.store.ts @@ -0,0 +1,31 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { makeAutoObservable } from 'mobx'; + +class TraceViewStore { + viewVisibleSpanView: boolean = false; + constructor() { + makeAutoObservable(this); + } + + public setVisibleSpanView(visible: boolean) { + this.viewVisibleSpanView = visible; + } +} + +export default new TraceViewStore(); diff --git a/web/src/styles/layout.scss b/web/src/styles/layout.scss index a4b8cab..567666f 100644 --- a/web/src/styles/layout.scss +++ b/web/src/styles/layout.scss @@ -233,3 +233,57 @@ under the License. align-items: center; justify-content: center; } + +.split-view { + height: 100vh; + position: absolute !important; + + .semi-sidesheet-header { + padding: 12px; + } + + .semi-timeline-item { + padding: 0 0 8px; + } + + .semi-sidesheet-body { + padding: 0; + + .left-handler { + width: 10px; + min-width: 10px; + position: relative; + cursor: col-resize; + left: 0 !important; + + &::before { + border-left: 1px solid transparent; + height: 100%; + left: 50%; + transform: translateX(-50%); + } + + &::after { + width: 4px; + height: 200px; + z-index: 9999; + background: var(--semi-color-primary); + content: ''; + position: fixed; + top: 50%; + transition: background 0.2s ease-in-out 0s; + transform: translate(-50%, -50%); + border-radius: 4px; + } + } + + .semi-tabs { + padding: 0 14px; + } + } + + .react-resizable-handle-se { + left: 0; + bottom: 50%; + } +} diff --git a/web/src/types/annotation.ts b/web/src/types/annotation.ts new file mode 100644 index 0000000..de9a5d6 --- /dev/null +++ b/web/src/types/annotation.ts @@ -0,0 +1,30 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +export const AnnotationPrefix = '__ann__'; + +export enum AnnotationType { + Span = 'span', + Alert = 'alert', +} + +export interface Annotation { + type: AnnotationType; + timestamp: number; + data?: object; +} diff --git a/web/src/types/datasource.ts b/web/src/types/datasource.ts index 2d290e4..90c7fde 100644 --- a/web/src/types/datasource.ts +++ b/web/src/types/datasource.ts @@ -17,8 +17,10 @@ under the License. */ import { ComponentType } from 'react'; import { DataSetType, Plugin, Variable } from '@src/types'; +import { cloneDeep } from 'lodash-es'; export enum DatasourceCategory { + Unknown = 'Unknown', Metric = 'metric', Log = 'log', Trace = 'trace', @@ -80,6 +82,7 @@ export interface DatasourceConstructor { class DatasourcePlugin extends Plugin { components: DatasourcePluginComponents = {}; + defaultParams: any = {}; constructor( public category: DatasourceCategory, @@ -101,11 +104,20 @@ class DatasourcePlugin extends Plugin { return this; } + setDefaultParams(params: any): DatasourcePlugin { + this.defaultParams = params; + return this; + } + setVariableEditor(VariableEditor: ComponentType): DatasourcePlugin { this.components.VariableEditor = VariableEditor; return this; } + getDefaultParams() { + return cloneDeep(this.defaultParams || {}); + } + getQueryEditor(): ComponentType { const editor = this.components.QueryEditor; if (editor) { diff --git a/web/src/types/feature.ts b/web/src/types/feature.ts index 5a37ac6..e019739 100644 --- a/web/src/types/feature.ts +++ b/web/src/types/feature.ts @@ -22,12 +22,20 @@ export class Feature { label: string; desc: string; component: ComponentType; + getDefaultSearchParams?: () => string; - constructor(key: string, label: string, desc: string, component: ComponentType) { + constructor( + key: string, + label: string, + desc: string, + component: ComponentType, + getDefaultSearchParams?: () => string + ) { this.key = key; this.label = label; this.desc = desc; this.component = component; + this.getDefaultSearchParams = getDefaultSearchParams; } } diff --git a/web/src/types/index.ts b/web/src/types/index.ts index bde2e77..e3c885d 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -29,3 +29,4 @@ export * from '@src/types/platform'; export * from '@src/types/format'; export * from '@src/types/dashboard'; export * from '@src/types/trace'; +export * from '@src/types/annotation'; diff --git a/web/src/types/platform.ts b/web/src/types/platform.ts index b8c0e2f..8b3b525 100644 --- a/web/src/types/platform.ts +++ b/web/src/types/platform.ts @@ -20,6 +20,7 @@ import { Preference, User } from './user'; import { Chart } from 'chart.js'; import DevIcon from '~devicon/devicon.json'; +import { replace } from 'lodash-es'; export interface Bootdata { home?: string; // home page @@ -64,6 +65,8 @@ export interface MouseEvent { native: any; index?: any; chart?: Chart; + series?: any; + timestamp?: number; } export interface IconItem { @@ -75,6 +78,8 @@ class IconRepository { constructor() { (DevIcon || []).forEach((i: any) => { this.icons.set(i.name, { name: i.name }); + // NOTE: (hack) fix apachekafka->kafka + this.icons.set(replace(i.name, 'apache', ''), { name: i.name }); }); } diff --git a/web/src/types/trace.ts b/web/src/types/trace.ts index 68b90f6..b4aff32 100644 --- a/web/src/types/trace.ts +++ b/web/src/types/trace.ts @@ -45,7 +45,7 @@ export interface Span { spanId: string; traceState: string; name: string; - kind: string; + kind: SpanKind; startTime: number; // ns endTime: number; // ns duration: number; // ns @@ -67,3 +67,26 @@ export interface Trace { process: Process; spans: Span[]; } + +export interface Exemplar { + traceId: string; + spanId: string; + duration: number; +} + +export enum SpanKind { + Unspecified = 'Unspecified', + Internal = 'Internal', + Server = 'Server', + Client = 'Client', + Producer = 'Producer', + Consumer = 'Consumer', +} + +// ref: https://github.com/open-telemetry/opentelemetry-collector/blob/main/semconv/v1.18.0/generated_trace.go +export enum TraceAttributes { + DBSystem = 'db.system', + RPCSystem = 'rpc.system', + MessagingSystem = 'messaging.system', + HTTPScheme = 'http.scheme', +} diff --git a/web/src/types/tracker.ts b/web/src/types/tracker.ts index bfca54d..5272741 100644 --- a/web/src/types/tracker.ts +++ b/web/src/types/tracker.ts @@ -15,13 +15,15 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { isEqual, cloneDeep } from 'lodash-es'; +import { cloneDeep } from 'lodash-es'; class Tracker { private val: T; + private strVal: string; constructor(initVal: T) { // NOTE: need clone val, avoid modify some object this.val = cloneDeep(initVal); + this.strVal = JSON.stringify(this.val); } getVal(): T { @@ -30,10 +32,11 @@ class Tracker { setNewVal(newVal: T) { this.val = cloneDeep(newVal); + this.strVal = JSON.stringify(this.val); } isChanged(newVal: T): boolean { - return !isEqual(this.val, newVal); + return this.strVal !== JSON.stringify(newVal); } } diff --git a/web/src/utils/dataset.ts b/web/src/utils/dataset.ts index 01cc67a..169eade 100644 --- a/web/src/utils/dataset.ts +++ b/web/src/utils/dataset.ts @@ -18,7 +18,7 @@ under the License. import { isEmpty, keys, forIn, get, trim, find } from 'lodash-es'; import { ColorKit } from '@src/utils'; import moment from 'moment'; -import { DataSetType, PanelSetting, Query } from '@src/types'; +import { DataSetType, Exemplar, PanelSetting, Query } from '@src/types'; const getGroupByTags = (tags?: any): string => { if (!tags) { @@ -94,7 +94,7 @@ const createTimeSeriesDatasets = (resultSet: any, panel: PanelSetting): any => { if (!rs) { return; } - const { series, startTime, endTime, interval, metricName } = rs; + const { series, startTime, endTime, interval, metricName, namespace } = rs; if (isEmpty(series)) { return; @@ -159,8 +159,11 @@ const createTimeSeriesDatasets = (resultSet: any, panel: PanelSetting): any => { stats, meta: { metricName, + namespace, tags, field: key, + interval, + target, }, }); }); @@ -209,6 +212,32 @@ const createDatasets = (resultSet: any, type: DataSetType, panel: PanelSetting): } }; +const createExemplarDatasets = (resultSet: any): Exemplar[] => { + const datasets: Exemplar[] = []; + forIn(resultSet, (rs: any, _: string) => { + if (!rs) { + return; + } + const { series } = rs; + if (isEmpty(series)) { + return; + } + series.forEach((item: any) => { + const { exemplars } = item; + if (!exemplars) { + return; + } + forIn(exemplars, (exemplar: any, _: string) => { + forIn(exemplar, (list: any, _: any) => { + datasets.push(...list); + }); + }); + }); + }); + return datasets; +}; + export default { createDatasets, + createExemplarDatasets, }; diff --git a/web/src/utils/datasource.ts b/web/src/utils/datasource.ts new file mode 100644 index 0000000..8e3fd4e --- /dev/null +++ b/web/src/utils/datasource.ts @@ -0,0 +1,51 @@ +/* +Licensed to LinDB under one or more contributor +license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright +ownership. LinDB licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +import { DatasourceStore } from '@src/stores'; +import { DatasourceCategory, DatasourceInstance } from '@src/types'; +import { set } from 'lodash-es'; +import { createSearchParams } from 'react-router-dom'; + +const isTrace = (datasource: DatasourceInstance | null | undefined): boolean => { + if (datasource && datasource.plugin) { + return datasource.plugin.category === DatasourceCategory.Trace; + } + return false; +}; + +const isMetric = (datasource: DatasourceInstance | null | undefined): boolean => { + if (datasource && datasource.plugin) { + return datasource.plugin.category === DatasourceCategory.Metric; + } + return false; +}; + +const getDatasourceDefaultParams = (datasourceUID: string): string => { + const datasource = DatasourceStore.getDatasource(datasourceUID); + if (!datasource) { + return ''; + } + const params = datasource?.plugin.getDefaultParams(); + set(params, 'datasource', { uid: datasource?.setting.uid, type: datasource?.plugin.Type }); + return createSearchParams({ left: JSON.stringify(params) }).toString(); +}; + +export default { + isTrace, + isMetric, + getDatasourceDefaultParams, +}; diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts index 95203f6..3ad1224 100644 --- a/web/src/utils/index.ts +++ b/web/src/utils/index.ts @@ -28,3 +28,5 @@ export { default as ChartKit } from '@src/utils/panel/chart'; export { default as StringKit } from '@src/utils/string'; export { default as DNDKit } from '@src/utils/dnd'; export { default as TimeKit } from '@src/utils/time'; +export { default as TraceKit } from '@src/utils/trace'; +export { default as DatasourceKit } from '@src/utils/datasource'; diff --git a/web/src/features/trace/util/trace.ts b/web/src/utils/trace.ts similarity index 74% rename from web/src/features/trace/util/trace.ts rename to web/src/utils/trace.ts index a6977b2..c9511de 100644 --- a/web/src/features/trace/util/trace.ts +++ b/web/src/utils/trace.ts @@ -15,9 +15,10 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Span, Trace } from '@src/types'; -import { forIn, map, groupBy } from 'lodash-es'; +import { Span, SpanKind, Trace, TraceAttributes } from '@src/types'; +import { forIn, map, groupBy, get, isEmpty } from 'lodash-es'; import type { Profile } from '@pyroscope/models/src'; +import { OneHour, OneMinute } from '@src/constants'; const groupSpans = (span: Span, tTotal: number, tStart: number) => { (span.children || []).forEach((x) => groupSpans(x, tTotal, tStart)); @@ -69,7 +70,7 @@ const buildTraceTree = (traces: Trace[]) => { //FIXME: need process no root - return tree; + return { tree, spanMap }; }; const toKeyValueList = (tags: object) => { @@ -90,7 +91,7 @@ const convertTraceToProfile = (traces: Trace[]): Profile => { levels: [] as number[][], }; - const tree = buildTraceTree(traces); + const { tree } = buildTraceTree(traces); if (tree) { // Step 3: traversing the tree @@ -129,8 +130,58 @@ const convertTraceToProfile = (traces: Trace[]): Profile => { }; }; +const createTraceDatasets = (resultSet: any): Trace[] => { + const datasets: Trace[] = []; + forIn(resultSet, (rs: any, _: string) => { + if (!rs) { + return; + } + datasets.push(...rs); + }); + return datasets; +}; + +const getSpanType = (span: Span): string => { + if (span.kind === SpanKind.Internal) { + return ''; + } + const attributes = [ + TraceAttributes.DBSystem, + TraceAttributes.RPCSystem, + TraceAttributes.MessagingSystem, + TraceAttributes.HTTPScheme, + ]; + + for (let attribute of attributes) { + const value = get(span.tags, attribute, ''); + if (!isEmpty(value)) { + return value; + } + } + return ''; +}; + +const calcMetricQueryTimeRange = (span: Span) => { + const ts = span.startTime / 1000000; // ns->ms + const now = new Date().getTime(); + if (ts + 30 * OneMinute < now) { + return { + from: ts - 30 * OneMinute, + to: ts + 30 * OneMinute, + }; + } else { + return { + from: now - OneHour, + to: now, + }; + } +}; + export default { + createTraceDatasets, buildTraceTree, toKeyValueList, convertTraceToProfile, + getSpanType, + calcMetricQueryTimeRange, }; diff --git a/web/static/index.html b/web/static/index.html index f790f20..52a4897 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -9,10 +9,10 @@ content="Web site created using create-react-app" /> - + Linsight - - + +
diff --git a/web/yarn.lock b/web/yarn.lock index 63afa97..a452cf3 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1086,10 +1086,10 @@ chart.js@^4.0.1: resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.0.1.tgz#93d5d50ac222a5b3b6ac7488e82e1553ac031592" integrity sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA== -chartjs-plugin-annotation@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz#8c307c931fda735a1acf1b606ad0e3fd7d96299b" - integrity sha512-kmEp2WtpogwnKKnDPO3iO3mVwvVGtmG5BkZVtAEZm5YzJ9CYxojjYEgk7OTrFbJ5vU098b84UeJRe8kRfNcq5g== +chartjs-plugin-annotation@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.0.1.tgz#4cf51d9797bf3202788ca0beae8694404621ad19" + integrity sha512-hlIrXXKqSDgb+ZjVYHefmlZUXK8KbkCPiynSVrTb/HjTMkT62cOInaT1NTQCKtxKKOm9oHp958DY3RTAFKtkHg== "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.5.3" @@ -2799,6 +2799,11 @@ raf-schd@^4.0.2: resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== +re-resizable@^6.9.11: + version "6.9.11" + resolved "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" + integrity sha512-a3hiLWck/NkmyLvGWUuvkAmN1VhwAz4yOhS6FdMTaxCUVN9joIWkT11wsO68coG/iEYuwn+p/7qAmfQzRhiPLQ== + react-beautiful-dnd@^13.1.1: version "13.1.1" resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"