From 1e8c21c7e730564a814704d63da6d079ff148157 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Thu, 25 Jul 2024 22:32:08 +0530 Subject: [PATCH 01/16] feat: Real-time telemetry --- src/components/Sidebar/Sidebar.jsx | 3 +- src/pages/Telemetry.jsx | 279 +++++++++++++++++++++++++++++ src/routes.jsx | 2 + 3 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/pages/Telemetry.jsx diff --git a/src/components/Sidebar/Sidebar.jsx b/src/components/Sidebar/Sidebar.jsx index 1191d816..ebc3d5b8 100644 --- a/src/components/Sidebar/Sidebar.jsx +++ b/src/components/Sidebar/Sidebar.jsx @@ -4,7 +4,7 @@ import { styled } from '@mui/material/styles'; import MuiDrawer from '@mui/material/Drawer'; import { List, Typography, Divider, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import { Link } from 'react-router-dom'; -import { LibraryBooks, Terminal, Animation, Key } from '@mui/icons-material'; +import { LibraryBooks, Terminal, Animation, Key, QueryStats } from '@mui/icons-material'; import Tooltip from '@mui/material/Tooltip'; import SidebarTutorialSection from './SidebarTutorialSection'; @@ -69,6 +69,7 @@ export default function Sidebar({ open, version, jwtEnabled }) { {sidebarItem('Datasets', , '/datasets', open)} + {sidebarItem('Telemetry', , '/telemetry', open)} {sidebarItem('Access Tokens', , '/jwt', open, jwtEnabled)} diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx new file mode 100644 index 00000000..53a5a7ee --- /dev/null +++ b/src/pages/Telemetry.jsx @@ -0,0 +1,279 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Button, + Card, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + Tooltip, + Typography, +} from '@mui/material'; +import { useClient } from '../context/client-context'; +import { CenteredFrame } from '../components/Common/CenteredFrame'; +import Chart from 'chart.js/auto'; +import { useTheme } from '@mui/material/styles'; +import { Info } from '@mui/icons-material'; + +function Telemetry() { + const [selectedEndpoint, setSelectedEndpoint] = useState('GET /telemetry'); + const [chartData, setChartData] = useState({}); + const [selectedEndpoints, setSelectedEndpoints] = useState([]); + const [refreshInterval, setRefreshInterval] = useState(1); + const [telemetryData, setTelemetryData] = useState(null); + const { client: qdrantClient } = useClient(); + const [chartInstances, setChartInstances] = useState({}); + const theme = useTheme(); + + useEffect(() => { + const fetchTelemetryData = async () => { + try { + const response = await qdrantClient.api('service').telemetry(); + setTelemetryData(response.data.result.requests.rest.responses); + + const currentTime = new Date().toLocaleTimeString(); + + selectedEndpoints.forEach((endpoint) => { + setChartData((prevData) => ({ + ...prevData, + [endpoint]: { + ...prevData[endpoint], + [currentTime]: response.data.result.requests.rest.responses[endpoint][200], + }, + })); + }); + } catch (error) { + console.error('Failed to fetch telemetry data', error); + } + }; + + fetchTelemetryData(); + + const intervalId = setInterval(() => { + fetchTelemetryData(); + }, refreshInterval * 1000); + + return () => clearInterval(intervalId); + }, [refreshInterval, selectedEndpoints]); + + const updateRefreshInterval = (interval) => { + setRefreshInterval(interval); + setChartData( + Object.keys(chartData).reduce((acc, endpoint) => { + acc[endpoint] = {}; + return acc; + }, {}) + ); + }; + + const addChart = (endpoint) => { + Object.keys(chartInstances).forEach((chart) => { + chartInstances[chart].destroy(); + }); + setChartInstances({}); + + setSelectedEndpoints((prevEndpoints) => [...prevEndpoints, endpoint]); + setChartData((prevData) => ({ + ...prevData, + [endpoint]: {}, + })); + }; + + useEffect(() => { + selectedEndpoints.forEach((endpoint) => { + const context = document.getElementById(endpoint); + + const chart = new Chart(context, { + type: 'line', + data: { + labels: Object.keys(chartData[endpoint]), + datasets: [ + { + label: 'Avg query latency(ms)', + data: Object.values(chartData[endpoint])?.map((value) => value?.avg_duration_micros) || [], + yAxisID: 'y', + }, + { + label: 'Query count', + data: Object.values(chartData[endpoint])?.map((value) => value?.count) || [], + yAxisID: 'y1', + }, + ], + }, + options: { + responsive: true, + interaction: { + mode: 'index', + intersect: false, + }, + stacked: false, + plugins: { + title: { + display: true, + text: endpoint, + }, + }, + scales: { + y: { + type: 'linear', + display: true, + position: 'left', + }, + y1: { + type: 'linear', + display: true, + position: 'right', + grid: { + drawOnChartArea: false, + }, + }, + }, + }, + }); + + setChartInstances((prevInstances) => ({ + ...prevInstances, + [endpoint]: chart, + })); + }); + }, [selectedEndpoints]); + + useEffect(() => { + syncColors(); + Object.keys(chartInstances).forEach((endpoint) => { + const chart = chartInstances[endpoint]; + chart.data.datasets[0].data = Object.values(chartData[endpoint]).map((value) => value?.avg_duration_micros) || []; + chart.data.datasets[1].data = Object.values(chartData[endpoint]).map((value) => value?.count) || []; + chart.data.labels = Object.keys(chartData[endpoint]); + chart.update(); + }); + }, [chartData]); + + const syncColors = () => { + Object.keys(chartInstances).forEach((endpoint) => { + const chart = chartInstances[endpoint]; + const color = theme.palette.mode === 'dark' ? 'white' : 'black'; + const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + + chart.options.scales.x.grid.color = gridColor; + chart.options.scales.y.grid.color = gridColor; + chart.options.scales.y1.grid.color = gridColor; + chart.options.plugins.title.color = color; + chart.options.scales.x.ticks.color = color; + chart.options.scales.y.ticks.color = color; + chart.options.scales.y1.ticks.color = color; + + chart.update(); + }); + }; + + useEffect(() => { + syncColors(); + }, [theme.palette.mode]); + + return ( + + + + Telemetry + + + + + + + + + + + + Refresh Interval + + + + + window.open('https://cloud.qdrant.io/')}> + + + + + + + + {selectedEndpoints.map((endpoint) => ( + + + + ))} + + + + ); +} + +export default Telemetry; diff --git a/src/routes.jsx b/src/routes.jsx index 2e6cbdb9..fa605021 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -8,6 +8,7 @@ import Tutorial from './pages/Tutorial'; import Datasets from './pages/Datasets'; import Jwt from './pages/Jwt'; import Graph from './pages/Graph'; +import Telemetry from './pages/Telemetry'; const routes = () => [ { @@ -30,6 +31,7 @@ const routes = () => [ { path: '/tutorial', element: }, { path: '/tutorial/:pageSlug', element: }, { path: '/jwt', element: }, + { path: '/telemetry', element: }, ], }, ]; From 67f9386a24dc6710d82814d736afcee5add1a212 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Fri, 26 Jul 2024 22:04:02 +0530 Subject: [PATCH 02/16] feat: adding info panel --- src/pages/Telemetry.jsx | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index 53a5a7ee..3987cae1 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -1,22 +1,22 @@ import React, { useEffect, useState } from 'react'; import { + Alert, Box, Button, Card, + Collapse, FormControl, Grid, - IconButton, InputLabel, + Link, MenuItem, Select, - Tooltip, Typography, } from '@mui/material'; import { useClient } from '../context/client-context'; import { CenteredFrame } from '../components/Common/CenteredFrame'; import Chart from 'chart.js/auto'; import { useTheme } from '@mui/material/styles'; -import { Info } from '@mui/icons-material'; function Telemetry() { const [selectedEndpoint, setSelectedEndpoint] = useState('GET /telemetry'); @@ -26,6 +26,8 @@ function Telemetry() { const [telemetryData, setTelemetryData] = useState(null); const { client: qdrantClient } = useClient(); const [chartInstances, setChartInstances] = useState({}); + const [open, setOpen] = useState(true); + const theme = useTheme(); useEffect(() => { @@ -180,6 +182,23 @@ function Telemetry() { Telemetry + + + { + setOpen(false); + }} + sx={{ mb: 2 }} + severity="info" + > + Looking for a full-scale monitoring solution? Our cloud platform offers advanced features and detailed + insights.{' '} + + Click here to explore more. + + + + 1 minute - - - window.open('https://cloud.qdrant.io/')}> - - - From 1a1877c17f8ef1ba2a97c2f97e0e8d7528666641 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Wed, 31 Jul 2024 23:32:00 +0530 Subject: [PATCH 03/16] Redesign telemetry (textual) and info panel --- src/components/Telemetry/Charts.jsx | 149 +++++++++ src/components/Telemetry/Editor.jsx | 101 ++++++ src/components/Telemetry/config/Rules.js | 92 ++++++ src/components/Telemetry/editor.css | 15 + src/pages/Telemetry.jsx | 379 +++++++---------------- 5 files changed, 463 insertions(+), 273 deletions(-) create mode 100644 src/components/Telemetry/Charts.jsx create mode 100644 src/components/Telemetry/Editor.jsx create mode 100644 src/components/Telemetry/config/Rules.js create mode 100644 src/components/Telemetry/editor.css diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx new file mode 100644 index 00000000..2b912371 --- /dev/null +++ b/src/components/Telemetry/Charts.jsx @@ -0,0 +1,149 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Box } from '@mui/material'; +import { bigIntJSON } from '../../common/bigIntJSON'; +import { useClient } from '../../context/client-context'; +import _ from 'lodash'; +import { Chart } from 'chart.js'; + +const Charts = ({ chartSpecsText }) => { + const [chartsData, setChartsData] = useState({}); + const [chartLabels, setChartLabels] = useState([]); + const { client: qdrantClient } = useClient(); + const [chartInstances, setChartInstances] = useState({}); + + useEffect(() => { + let intervalId; + + const initializeCharts = async () => { + if (!chartSpecsText) { + return; + } + + let requestBody; + + try { + requestBody = bigIntJSON.parse(chartSpecsText); + } catch (e) { + console.error('Invalid JSON:', e); + return; + } + + if (requestBody.paths) { + const fetchTelemetryData = async () => { + try { + const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); + requestBody.paths?.forEach((path) => { + const data = _.get(response.data.result, path, 0); + + setChartsData((prevData) => ({ + ...prevData, + [path]: { + ...prevData[path], + [new Date().toLocaleTimeString()]: data, + }, + })); + }); + } catch (error) { + console.error('Failed to fetch telemetry data', error); + } + }; + + await fetchTelemetryData(); + + if (!requestBody.paths) { + console.error('Invalid request body:', requestBody); + return; + } else { + setChartLabels(requestBody.paths); + } + if (requestBody.reload_interval) { + intervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); + } + } + }; + initializeCharts(); + + return () => { + clearInterval(intervalId); + setChartsData({}); + setChartLabels([]); + }; + }, [chartSpecsText, qdrantClient]); + + useEffect(() => { + Object.keys(chartInstances).forEach((chart) => { + chartInstances[chart].destroy(); + }); + setChartInstances({}); + + const createChart = (path) => { + const context = document.getElementById(path); + const newChart = new Chart(context, { + type: 'line', + data: { + labels: Object.keys(chartsData[path]), + datasets: [ + { + label: path, + data: Object.values(chartsData[path]), + }, + ], + }, + + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + }, + }); + setChartInstances((prevInstances) => ({ + ...prevInstances, + [path]: newChart, + })); + }; + + chartLabels.forEach(createChart); + }, [chartLabels]); + + useEffect(() => { + if (Object.keys(chartsData).length === 0) { + return; + } + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + chart.data.labels = Object.keys(chartsData[path]) ?? []; + chart.data.datasets[0].data = Object.values(chartsData[path]) ?? []; + chart.update(); + }); + }, [chartsData]); + + return ( + <> + {Object.keys(chartsData).map((path) => ( + + + + ))} + + ); +}; + +Charts.propTypes = { + chartSpecsText: PropTypes.string.isRequired, +}; + +export default Charts; diff --git a/src/components/Telemetry/Editor.jsx b/src/components/Telemetry/Editor.jsx new file mode 100644 index 00000000..d96bb951 --- /dev/null +++ b/src/components/Telemetry/Editor.jsx @@ -0,0 +1,101 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { options, btnconfig, getCodeBlocks, selectBlock } from './config/Rules'; +import { useTheme } from '@mui/material/styles'; +import './editor.css'; +import EditorCommon from '../EditorCommon'; + +const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { + const editorRef = useRef(null); + const lensesRef = useRef(null); + const autocompleteRef = useRef(null); + let runBtnCommandId = null; + + const theme = useTheme(); + + useEffect( + () => () => { + lensesRef.current?.dispose(); + autocompleteRef.current?.dispose(); + }, + [] + ); + + function handleEditorDidMount(editor, monaco) { + editorRef.current = editor; + let decorations = []; + + runBtnCommandId = editor.addCommand( + 0, + async (_ctx, ...args) => { + const data = args[0]; + onChangeResult(data); + }, + '' + ); + + // Register Code Lens Provider (Run Button) + lensesRef.current = monaco.languages.registerCodeLensProvider('custom-language', btnconfig(runBtnCommandId)); + + // Listen for Mouse Postion Change + editor.onDidChangeCursorPosition(() => { + const currentCode = editor.getValue(); + const currentBlocks = getCodeBlocks(currentCode); + + const selectedCodeBlock = selectBlock(currentBlocks, editor.getPosition().lineNumber); + + monaco.editor.selectedCodeBlock = selectedCodeBlock; + + if (selectedCodeBlock) { + const fromRange = selectedCodeBlock.blockStartLine; + const toRange = selectedCodeBlock.blockEndLine; + // Make the decortion on the selected range + decorations = editor.deltaDecorations( + [decorations[0]], + [ + { + range: new monaco.Range(fromRange, 0, toRange, 3), + options: { + className: theme.palette.mode === 'dark' ? 'blockSelector' : 'blockSelector', + glyphMarginClassName: theme.palette.mode === 'dark' ? 'blockSelectorStrip' : 'blockSelectorStrip', + isWholeLine: true, + }, + }, + ] + ); + editor.addCommand(monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter, async () => { + const data = selectedCodeBlock.blockText; + onChangeResult(data); + }); + } + }); + } + // function handleEditorWillMount(monaco) { + // autocomplete(monaco, qdrantClient, collectionName).then((autocomplete) => { + // autocompleteRef.current = monaco.languages.registerCompletionItemProvider('custom-language', autocomplete); + // }); + // } + + return ( + + ); +}; + +CodeEditorWindow.propTypes = { + onChange: PropTypes.func.isRequired, + code: PropTypes.string.isRequired, + onChangeResult: PropTypes.func.isRequired, +}; +export default CodeEditorWindow; diff --git a/src/components/Telemetry/config/Rules.js b/src/components/Telemetry/config/Rules.js new file mode 100644 index 00000000..fc875e66 --- /dev/null +++ b/src/components/Telemetry/config/Rules.js @@ -0,0 +1,92 @@ +export const options = { + scrollBeyondLastLine: false, + readOnly: false, + fontSize: 12, + wordWrap: 'on', + minimap: { enabled: false }, + automaticLayout: true, + mouseWheelZoom: true, + glyphMargin: true, + wordBasedSuggestions: false, +}; + +export function btnconfig(commandId) { + return { + // function takes model and token as arguments + provideCodeLenses: function (model) { + const codeBlocks = getCodeBlocks(model.getValue()); + const lenses = []; + + for (let i = 0; i < codeBlocks.length; ++i) { + lenses.push({ + range: { + startLineNumber: codeBlocks[i].blockStartLine, + startColumn: 1, + endLineNumber: codeBlocks[i].blockStartLine, + endColumn: 1, + }, + id: 'RUN', + command: { + id: commandId, + title: 'RUN', + arguments: [codeBlocks[i].blockText], + }, + }); + } + + return { + lenses: lenses, + dispose: () => {}, + }; + }, + // function takes model, codeLens and token as arguments + resolveCodeLens: function (model, codeLens) { + return codeLens; + }, + }; +} + +export function selectBlock(blocks, location) { + for (let i = 0; i < blocks.length; ++i) { + if (blocks[i].blockStartLine <= location && location <= blocks[i].blockEndLine) { + return blocks[i]; + } + } + return null; +} + +export function getCodeBlocks(codeText) { + const codeArray = codeText.replace(/\/\/.*$/gm, '').split(/\r?\n/); + const blocksArray = []; + let block = { blockText: '', blockStartLine: null, blockEndLine: null }; + let backetcount = 0; + let codeStarLine = 0; + let codeEndline = 0; + for (let i = 0; i < codeArray.length; ++i) { + // dealing for request which have JSON Body + if (codeArray[i].includes('{')) { + if (backetcount === 0) { + codeStarLine = i + 1; + } + backetcount = backetcount + codeArray[i].match(/{/gi).length; + } + if (codeArray[i].includes('}')) { + backetcount = backetcount - codeArray[i].match(/}/gi).length; + if (backetcount === 0) { + codeEndline = i + 1; + } + } + if (codeStarLine) { + block.blockStartLine = codeStarLine; + block.blockText = block.blockText + codeArray[i] + '\n'; + if (codeEndline) { + block.blockEndLine = codeEndline; + blocksArray.push(block); + codeEndline = 0; + codeStarLine = 0; + block = { blockText: '', blockStartLine: null, blockEndLine: null }; + } + } + } + return blocksArray; +} diff --git a/src/components/Telemetry/editor.css b/src/components/Telemetry/editor.css new file mode 100644 index 00000000..01e85b60 --- /dev/null +++ b/src/components/Telemetry/editor.css @@ -0,0 +1,15 @@ +.light .blockSelector { + background: rgba(163, 161, 161, 0.24); +} + +.light .blockSelectorStrip { + background: rgba(255, 252, 88, 0.87); +} + +.dark .blockSelector { + background: #2d2d3099; +} + +.dark .blockSelectorStrip { + background: #525100cc; +} diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index 3987cae1..e5894764 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -1,288 +1,121 @@ -import React, { useEffect, useState } from 'react'; -import { - Alert, - Box, - Button, - Card, - Collapse, - FormControl, - Grid, - InputLabel, - Link, - MenuItem, - Select, - Typography, -} from '@mui/material'; -import { useClient } from '../context/client-context'; -import { CenteredFrame } from '../components/Common/CenteredFrame'; -import Chart from 'chart.js/auto'; -import { useTheme } from '@mui/material/styles'; - +import React, { useState } from 'react'; +import { Alert, Box, Collapse, Grid, Link } from '@mui/material'; +import { alpha, useTheme } from '@mui/material/styles'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; +import TelemetryEditorWindow from '../components/Telemetry/Editor'; +import Charts from '../components/Telemetry/Charts'; + +const query = ` + +// app.system.ram_size +// app.system.disk_size +// collections.number_of_collections +// collections.collections[0].init_time_ms +// collections.collections[0].config.params.vectors.size +// collections.collections[0].config.params.shard_number +// collections.collections[0].config.params.replication_factor +// collections.collections[0].config.params.write_consistency_factor +// collections.collections[0].config.hnsw_config.m +// collections.collections[0].config.hnsw_config.ef_construct +// collections.collections[0].config.hnsw_config.full_scan_threshold +// collections.collections[0].config.hnsw_config.max_indexing_threads +// collections.collections[0].config.optimizer_config.deleted_threshold +// collections.collections[0].config.optimizer_config.vacuum_min_vector_number +// collections.collections[0].config.optimizer_config.default_segment_number +// collections.collections[0].config.optimizer_config.indexing_threshold +// collections.collections[0].config.optimizer_config.flush_interval_sec +// collections.collections[0].config.optimizer_config.max_optimization_threads +// collections.collections[0].config.wal_config.wal_capacity_mb +// collections.collections[0].config.wal_config.wal_segments_ahead + + + +{ + "reload_interval": 2, + "paths": [ + "app.system.disk_size", + "app.system.ram_size", + "collections.collections[0].shards[0].local.segments[0].info.num_indexed_vectors", + "requests.rest.responses['GET /telemetry'][200].count", + "requests.rest.responses['OPTIONS /collections'][200].count" + ] +}`; +const defaultResult = '{}'; function Telemetry() { - const [selectedEndpoint, setSelectedEndpoint] = useState('GET /telemetry'); - const [chartData, setChartData] = useState({}); - const [selectedEndpoints, setSelectedEndpoints] = useState([]); - const [refreshInterval, setRefreshInterval] = useState(1); - const [telemetryData, setTelemetryData] = useState(null); - const { client: qdrantClient } = useClient(); - const [chartInstances, setChartInstances] = useState({}); + const [code, setCode] = useState(query); + const [result, setResult] = useState(defaultResult); const [open, setOpen] = useState(true); - const theme = useTheme(); - useEffect(() => { - const fetchTelemetryData = async () => { - try { - const response = await qdrantClient.api('service').telemetry(); - setTelemetryData(response.data.result.requests.rest.responses); - - const currentTime = new Date().toLocaleTimeString(); - - selectedEndpoints.forEach((endpoint) => { - setChartData((prevData) => ({ - ...prevData, - [endpoint]: { - ...prevData[endpoint], - [currentTime]: response.data.result.requests.rest.responses[endpoint][200], - }, - })); - }); - } catch (error) { - console.error('Failed to fetch telemetry data', error); - } - }; - - fetchTelemetryData(); - - const intervalId = setInterval(() => { - fetchTelemetryData(); - }, refreshInterval * 1000); - - return () => clearInterval(intervalId); - }, [refreshInterval, selectedEndpoints]); - - const updateRefreshInterval = (interval) => { - setRefreshInterval(interval); - setChartData( - Object.keys(chartData).reduce((acc, endpoint) => { - acc[endpoint] = {}; - return acc; - }, {}) - ); - }; - - const addChart = (endpoint) => { - Object.keys(chartInstances).forEach((chart) => { - chartInstances[chart].destroy(); - }); - setChartInstances({}); - - setSelectedEndpoints((prevEndpoints) => [...prevEndpoints, endpoint]); - setChartData((prevData) => ({ - ...prevData, - [endpoint]: {}, - })); - }; - - useEffect(() => { - selectedEndpoints.forEach((endpoint) => { - const context = document.getElementById(endpoint); - - const chart = new Chart(context, { - type: 'line', - data: { - labels: Object.keys(chartData[endpoint]), - datasets: [ - { - label: 'Avg query latency(ms)', - data: Object.values(chartData[endpoint])?.map((value) => value?.avg_duration_micros) || [], - yAxisID: 'y', - }, - { - label: 'Query count', - data: Object.values(chartData[endpoint])?.map((value) => value?.count) || [], - yAxisID: 'y1', - }, - ], - }, - options: { - responsive: true, - interaction: { - mode: 'index', - intersect: false, - }, - stacked: false, - plugins: { - title: { - display: true, - text: endpoint, - }, - }, - scales: { - y: { - type: 'linear', - display: true, - position: 'left', - }, - y1: { - type: 'linear', - display: true, - position: 'right', - grid: { - drawOnChartArea: false, - }, - }, - }, - }, - }); - - setChartInstances((prevInstances) => ({ - ...prevInstances, - [endpoint]: chart, - })); - }); - }, [selectedEndpoints]); - - useEffect(() => { - syncColors(); - Object.keys(chartInstances).forEach((endpoint) => { - const chart = chartInstances[endpoint]; - chart.data.datasets[0].data = Object.values(chartData[endpoint]).map((value) => value?.avg_duration_micros) || []; - chart.data.datasets[1].data = Object.values(chartData[endpoint]).map((value) => value?.count) || []; - chart.data.labels = Object.keys(chartData[endpoint]); - chart.update(); - }); - }, [chartData]); - - const syncColors = () => { - Object.keys(chartInstances).forEach((endpoint) => { - const chart = chartInstances[endpoint]; - const color = theme.palette.mode === 'dark' ? 'white' : 'black'; - const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - - chart.options.scales.x.grid.color = gridColor; - chart.options.scales.y.grid.color = gridColor; - chart.options.scales.y1.grid.color = gridColor; - chart.options.plugins.title.color = color; - chart.options.scales.x.ticks.color = color; - chart.options.scales.y.ticks.color = color; - chart.options.scales.y1.ticks.color = color; - - chart.update(); - }); - }; - - useEffect(() => { - syncColors(); - }, [theme.palette.mode]); - return ( - - - - Telemetry - - - - { - setOpen(false); - }} - sx={{ mb: 2 }} - severity="info" - > - Looking for a full-scale monitoring solution? Our cloud platform offers advanced features and detailed - insights.{' '} - - Click here to explore more. - - - - - - + + + - - - - - - - - - Refresh Interval - - - - - - - {selectedEndpoints.map((endpoint) => ( - - - - ))} + ⋮ + + + + + + + - - + + ); } From e07e31fb6cede49daeaa46bc2d166d01c14564b4 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Thu, 1 Aug 2024 00:26:12 +0530 Subject: [PATCH 04/16] add basic error handling --- src/components/Telemetry/Charts.jsx | 147 ++++++++++++++++++++++++++-- src/pages/Telemetry.jsx | 34 +++---- 2 files changed, 149 insertions(+), 32 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 2b912371..e926a481 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,22 +1,85 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Box } from '@mui/material'; +import { Alert, Box, Collapse, Link } from '@mui/material'; import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; import { Chart } from 'chart.js'; +const AlertComponent = ({ alert, index, setAlerts }) => { + const [open, setOpen] = useState(true); + useEffect(() => { + if (alert.autoClose) { + setTimeout(() => { + setOpen(false); + }, 5000); + } + }, [alert.autoClose]); + + return ( + { + if (!open) { + setAlerts((prevAlerts) => prevAlerts.filter((_, i) => i !== index)); + } + }} + > + { + setOpen(false); + console.log('index', setAlerts); + }} + severity={alert.severity} + > + {alert.message} + + + ); +}; + +AlertComponent.propTypes = { + alert: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + setAlerts: PropTypes.func.isRequired, +}; + const Charts = ({ chartSpecsText }) => { const [chartsData, setChartsData] = useState({}); const [chartLabels, setChartLabels] = useState([]); const { client: qdrantClient } = useClient(); const [chartInstances, setChartInstances] = useState({}); + // const [open, setOpen] = useState(true); + const [alerts, setAlerts] = useState([ + { + severity: 'info', + message: ( + <> + Looking for a full-scale monitoring solution? Our cloud platform offers advanced features and detailed + insights.{' '} + + Click here to explore more. + + + ), + autoClose: false, + }, + ]); useEffect(() => { let intervalId; const initializeCharts = async () => { if (!chartSpecsText) { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'info', + message: 'No chart specs provided. Please provide a valid JSON to render charts.', + autoClose: true, + }, + ]); return; } @@ -25,11 +88,67 @@ const Charts = ({ chartSpecsText }) => { try { requestBody = bigIntJSON.parse(chartSpecsText); } catch (e) { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'Invalid JSON provided. Please provide a valid JSON to render charts.', + autoClose: true, + }, + ]); console.error('Invalid JSON:', e); return; } - - if (requestBody.paths) { + if (!requestBody.paths) { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'Invalid request body. Please provide a valid JSON to render charts.', + autoClose: true, + }, + ]); + console.error('Invalid request body:', requestBody); + return; + } else if (!Array.isArray(requestBody.paths)) { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'Invalid paths provided. Please provide an array of paths to render charts.', + autoClose: true, + }, + ]); + console.error('Invalid paths:', requestBody.paths); + return; + } else if (requestBody.paths.length === 0) { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'No paths provided. Please provide at least one path to render charts.', + autoClose: true, + }, + ]); + console.error('No paths provided:', requestBody.paths); + return; + } else if (requestBody.reload_interval && typeof requestBody.reload_interval !== 'number') { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'Invalid reload interval provided. Please provide a valid number to reload charts.', + autoClose: true, + }, + ]); + console.error('Invalid reload interval:', requestBody.reload_interval); + return; + } else if ( + requestBody.paths && + requestBody.paths.length > 0 && + requestBody.reload_interval && + typeof requestBody.reload_interval === 'number' + ) { const fetchTelemetryData = async () => { try { const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); @@ -51,15 +170,22 @@ const Charts = ({ chartSpecsText }) => { await fetchTelemetryData(); - if (!requestBody.paths) { - console.error('Invalid request body:', requestBody); - return; - } else { - setChartLabels(requestBody.paths); - } + setChartLabels(requestBody.paths); + if (requestBody.reload_interval) { intervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); } + } else { + setAlerts((prevAlerts) => [ + ...prevAlerts, + { + severity: 'error', + message: 'Invalid request body. Please provide a valid JSON to render charts.', + autoClose: true, + }, + ]); + console.error('Invalid request body:', requestBody); + return; } }; initializeCharts(); @@ -123,6 +249,9 @@ const Charts = ({ chartSpecsText }) => { return ( <> + {alerts.map((alert, index) => ( + + ))} {Object.keys(chartsData).map((path) => ( - - { - setOpen(false); - }} - severity="info" - > - Looking for a full-scale monitoring solution? Our cloud platform offers advanced features and - detailed insights.{' '} - - Click here to explore more. - - - From 300ac84e2c4aeaca0a00423fc19a44fd029186e2 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 15:54:57 +0530 Subject: [PATCH 05/16] feat: realtime view graph added --- package-lock.json | 62 +++++++++++++++++++++++++++++ package.json | 3 ++ src/components/Telemetry/Charts.jsx | 49 +++++++++++++++++------ src/pages/Telemetry.jsx | 11 +---- 4 files changed, 103 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 128d07d3..4e303aab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@mui/material": "^5.11.15", "@mui/x-data-grid": "^6.0.4", "@qdrant/js-client-rest": "^1.10.0", + "@robloche/chartjs-plugin-streaming": "^3.1.0", "@saehrimnir/druidjs": "^0.6.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -33,6 +34,7 @@ "autocomplete-openapi": "0.1.4", "axios": "^1.6.7", "chart.js": "^4.3.0", + "chartjs-adapter-luxon": "^1.3.1", "chroma-js": "^2.4.2", "force-graph": "^1.43.5", "jose": "^5.2.3", @@ -47,6 +49,7 @@ "prismjs": "^1.29.0", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18.2.0", "react-resizable-panels": "^0.0.51", @@ -1719,6 +1722,14 @@ "node": ">=14" } }, + "node_modules/@robloche/chartjs-plugin-streaming": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@robloche/chartjs-plugin-streaming/-/chartjs-plugin-streaming-3.1.0.tgz", + "integrity": "sha512-6tTRo0eyw7klsk2ayjrZmJga+NqawDxwXlMOsQGAqJ8kZj9szNwlfD9EmTP2MDA8Sh8qFezxE6LYU6UUoothkg==", + "peerDependencies": { + "chart.js": "^4.1.1" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -3091,6 +3102,15 @@ "pnpm": ">=7" } }, + "node_modules/chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "luxon": ">=1.0.0" + } + }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -5575,6 +5595,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -7233,6 +7262,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-diff-viewer-continued": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", @@ -10006,6 +10044,12 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz", "integrity": "sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q==" }, + "@robloche/chartjs-plugin-streaming": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@robloche/chartjs-plugin-streaming/-/chartjs-plugin-streaming-3.1.0.tgz", + "integrity": "sha512-6tTRo0eyw7klsk2ayjrZmJga+NqawDxwXlMOsQGAqJ8kZj9szNwlfD9EmTP2MDA8Sh8qFezxE6LYU6UUoothkg==", + "requires": {} + }, "@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -10994,6 +11038,12 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-adapter-luxon": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-luxon/-/chartjs-adapter-luxon-1.3.1.tgz", + "integrity": "sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==", + "requires": {} + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -12736,6 +12786,12 @@ "yallist": "^3.0.2" } }, + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "peer": true + }, "lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -13793,6 +13849,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "requires": {} + }, "react-diff-viewer-continued": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", diff --git a/package.json b/package.json index d514706b..86341019 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@mui/material": "^5.11.15", "@mui/x-data-grid": "^6.0.4", "@qdrant/js-client-rest": "^1.10.0", + "@robloche/chartjs-plugin-streaming": "^3.1.0", "@saehrimnir/druidjs": "^0.6.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -28,6 +29,7 @@ "autocomplete-openapi": "0.1.4", "axios": "^1.6.7", "chart.js": "^4.3.0", + "chartjs-adapter-luxon": "^1.3.1", "chroma-js": "^2.4.2", "force-graph": "^1.43.5", "jose": "^5.2.3", @@ -42,6 +44,7 @@ "prismjs": "^1.29.0", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-diff-viewer-continued": "^3.4.0", "react-dom": "^18.2.0", "react-resizable-panels": "^0.0.51", diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index e926a481..aabac76e 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -5,14 +5,21 @@ import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; import { Chart } from 'chart.js'; +import StreamingPlugin from '@robloche/chartjs-plugin-streaming'; +import 'chartjs-adapter-luxon'; + +Chart.register(StreamingPlugin); const AlertComponent = ({ alert, index, setAlerts }) => { const [open, setOpen] = useState(true); + useEffect(() => { if (alert.autoClose) { - setTimeout(() => { + const timer = setTimeout(() => { setOpen(false); }, 5000); + + return () => clearTimeout(timer); } }, [alert.autoClose]); @@ -29,7 +36,6 @@ const AlertComponent = ({ alert, index, setAlerts }) => { { setOpen(false); - console.log('index', setAlerts); }} severity={alert.severity} > @@ -50,7 +56,8 @@ const Charts = ({ chartSpecsText }) => { const [chartLabels, setChartLabels] = useState([]); const { client: qdrantClient } = useClient(); const [chartInstances, setChartInstances] = useState({}); - // const [open, setOpen] = useState(true); + const [reloadInterval, setReloadInterval] = useState(2); + const [intervalId, setIntervalId] = useState(null); // Store a single interval ID const [alerts, setAlerts] = useState([ { severity: 'info', @@ -68,8 +75,6 @@ const Charts = ({ chartSpecsText }) => { ]); useEffect(() => { - let intervalId; - const initializeCharts = async () => { if (!chartSpecsText) { setAlerts((prevAlerts) => [ @@ -171,9 +176,16 @@ const Charts = ({ chartSpecsText }) => { await fetchTelemetryData(); setChartLabels(requestBody.paths); + setReloadInterval(requestBody.reload_interval); if (requestBody.reload_interval) { - intervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); + if (intervalId) { + console.log('Clearing interval ID:', intervalId); + clearInterval(intervalId); + } + const newIntervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); + console.log('Setting interval ID:', newIntervalId); + setIntervalId(newIntervalId); } } else { setAlerts((prevAlerts) => [ @@ -188,14 +200,18 @@ const Charts = ({ chartSpecsText }) => { return; } }; + initializeCharts(); return () => { - clearInterval(intervalId); + if (intervalId) { + console.log('Clearing interval ID:', intervalId); + clearInterval(intervalId); + } setChartsData({}); setChartLabels([]); }; - }, [chartSpecsText, qdrantClient]); + }, [chartSpecsText]); useEffect(() => { Object.keys(chartInstances).forEach((chart) => { @@ -208,11 +224,11 @@ const Charts = ({ chartSpecsText }) => { const newChart = new Chart(context, { type: 'line', data: { - labels: Object.keys(chartsData[path]), + labels: [], datasets: [ { label: path, - data: Object.values(chartsData[path]), + data: [], }, ], }, @@ -224,6 +240,15 @@ const Charts = ({ chartSpecsText }) => { mode: 'index', intersect: false, }, + animation: false, + scales: { + x: { + type: 'realtime', + realtime: { + delay: reloadInterval * 1000, + }, + }, + }, }, }); setChartInstances((prevInstances) => ({ @@ -233,7 +258,7 @@ const Charts = ({ chartSpecsText }) => { }; chartLabels.forEach(createChart); - }, [chartLabels]); + }, [chartLabels, reloadInterval]); useEffect(() => { if (Object.keys(chartsData).length === 0) { @@ -275,4 +300,4 @@ Charts.propTypes = { chartSpecsText: PropTypes.string.isRequired, }; -export default Charts; +export default Charts diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index 4eb56c5f..ee51d57c 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -40,16 +40,7 @@ const query = ` "requests.rest.responses['OPTIONS /collections'][200].count" ] }`; -const defaultResult = `{ - "reload_interval": 2, - "paths": [ - "app.system.disk_size", - "app.system.ram_size", - "collections.collections[0].shards[0].local.segments[0].info.num_indexed_vectors", - "requests.rest.responses['GET /telemetry'][200].count", - "requests.rest.responses['OPTIONS /collections'][200].count" - ] -}`; +const defaultResult = ``; function Telemetry() { const [code, setCode] = useState(query); const [result, setResult] = useState(defaultResult); From 808e89af157de5589281b58ce79d25b45f4bd146 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 15:57:03 +0530 Subject: [PATCH 06/16] fmt --- src/components/Telemetry/Charts.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index aabac76e..50270617 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -182,7 +182,7 @@ const Charts = ({ chartSpecsText }) => { if (intervalId) { console.log('Clearing interval ID:', intervalId); clearInterval(intervalId); - } + } const newIntervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); console.log('Setting interval ID:', newIntervalId); setIntervalId(newIntervalId); @@ -211,7 +211,7 @@ const Charts = ({ chartSpecsText }) => { setChartsData({}); setChartLabels([]); }; - }, [chartSpecsText]); + }, [chartSpecsText]); useEffect(() => { Object.keys(chartInstances).forEach((chart) => { @@ -300,4 +300,4 @@ Charts.propTypes = { chartSpecsText: PropTypes.string.isRequired, }; -export default Charts +export default Charts; From 202a327e5cf26d299ffa032e80a89f89b30a2828 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 20:04:49 +0530 Subject: [PATCH 07/16] fix --- src/components/Telemetry/Charts.jsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 50270617..0c2fd6c4 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -57,7 +57,7 @@ const Charts = ({ chartSpecsText }) => { const { client: qdrantClient } = useClient(); const [chartInstances, setChartInstances] = useState({}); const [reloadInterval, setReloadInterval] = useState(2); - const [intervalId, setIntervalId] = useState(null); // Store a single interval ID + const [intervalId, setIntervalId] = useState(null); const [alerts, setAlerts] = useState([ { severity: 'info', @@ -180,11 +180,9 @@ const Charts = ({ chartSpecsText }) => { if (requestBody.reload_interval) { if (intervalId) { - console.log('Clearing interval ID:', intervalId); clearInterval(intervalId); } const newIntervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); - console.log('Setting interval ID:', newIntervalId); setIntervalId(newIntervalId); } } else { @@ -205,7 +203,6 @@ const Charts = ({ chartSpecsText }) => { return () => { if (intervalId) { - console.log('Clearing interval ID:', intervalId); clearInterval(intervalId); } setChartsData({}); @@ -224,15 +221,14 @@ const Charts = ({ chartSpecsText }) => { const newChart = new Chart(context, { type: 'line', data: { - labels: [], + labels: Object.keys(chartsData[path]) ?? [], datasets: [ { label: path, - data: [], + data: Object.values(chartsData[path]) ?? [], }, ], }, - options: { responsive: true, maintainAspectRatio: false, @@ -240,12 +236,11 @@ const Charts = ({ chartSpecsText }) => { mode: 'index', intersect: false, }, - animation: false, scales: { x: { type: 'realtime', realtime: { - delay: reloadInterval * 1000, + duration: reloadInterval * 5000, }, }, }, @@ -268,7 +263,7 @@ const Charts = ({ chartSpecsText }) => { const chart = chartInstances[path]; chart.data.labels = Object.keys(chartsData[path]) ?? []; chart.data.datasets[0].data = Object.values(chartsData[path]) ?? []; - chart.update(); + chart.update('quiet'); }); }, [chartsData]); From c85083e3ac77fc1590231a1f55aa103bc6fd7d1e Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 23:23:37 +0530 Subject: [PATCH 08/16] delay added --- src/components/Telemetry/Charts.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 0c2fd6c4..a9d33e91 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -240,7 +240,8 @@ const Charts = ({ chartSpecsText }) => { x: { type: 'realtime', realtime: { - duration: reloadInterval * 5000, + duration: reloadInterval * 4000, + delay: 3000, }, }, }, From 81270b27e237255795b5fe07c09c5ad22b2aa774 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 23:31:32 +0530 Subject: [PATCH 09/16] syncColors added --- src/components/Telemetry/Charts.jsx | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index a9d33e91..8e85a275 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Alert, Box, Collapse, Link } from '@mui/material'; +import { Alert, Box, Collapse, Link, useTheme } from '@mui/material'; import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; @@ -58,6 +58,7 @@ const Charts = ({ chartSpecsText }) => { const [chartInstances, setChartInstances] = useState({}); const [reloadInterval, setReloadInterval] = useState(2); const [intervalId, setIntervalId] = useState(null); + const theme = useTheme(); const [alerts, setAlerts] = useState([ { severity: 'info', @@ -268,6 +269,26 @@ const Charts = ({ chartSpecsText }) => { }); }, [chartsData]); + const syncColors = () => { + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + const color = theme.palette.mode === 'dark' ? 'white' : 'black'; + const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + + chart.options.scales.x.grid.color = gridColor; + chart.options.scales.y.grid.color = gridColor; + chart.options.plugins.title.color = color; + chart.options.scales.x.ticks.color = color; + chart.options.scales.y.ticks.color = color; + + chart.update(); + }); + }; + + useEffect(() => { + syncColors(); + }, [theme.palette.mode]); + return ( <> {alerts.map((alert, index) => ( From e524ca882151a312ee776c93d27a263352e30ccc Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 23:32:56 +0530 Subject: [PATCH 10/16] add colorsync to intail load --- src/components/Telemetry/Charts.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 8e85a275..f1a5a250 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -248,6 +248,8 @@ const Charts = ({ chartSpecsText }) => { }, }, }); + + syncColors(newChart); setChartInstances((prevInstances) => ({ ...prevInstances, [path]: newChart, @@ -269,24 +271,22 @@ const Charts = ({ chartSpecsText }) => { }); }, [chartsData]); - const syncColors = () => { - Object.keys(chartInstances).forEach((path) => { - const chart = chartInstances[path]; + const syncColors = (chart) => { const color = theme.palette.mode === 'dark' ? 'white' : 'black'; const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - chart.options.scales.x.grid.color = gridColor; chart.options.scales.y.grid.color = gridColor; chart.options.plugins.title.color = color; chart.options.scales.x.ticks.color = color; chart.options.scales.y.ticks.color = color; - chart.update(); - }); }; useEffect(() => { - syncColors(); + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + syncColors(chart); + }); }, [theme.palette.mode]); return ( From bd5663eb71b3e6d403f62583d88ea966d3a9b133 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Mon, 5 Aug 2024 23:39:24 +0530 Subject: [PATCH 11/16] fmt --- src/components/Telemetry/Charts.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index f1a5a250..3c625a19 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -248,7 +248,7 @@ const Charts = ({ chartSpecsText }) => { }, }, }); - + syncColors(newChart); setChartInstances((prevInstances) => ({ ...prevInstances, @@ -272,14 +272,14 @@ const Charts = ({ chartSpecsText }) => { }, [chartsData]); const syncColors = (chart) => { - const color = theme.palette.mode === 'dark' ? 'white' : 'black'; - const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - chart.options.scales.x.grid.color = gridColor; - chart.options.scales.y.grid.color = gridColor; - chart.options.plugins.title.color = color; - chart.options.scales.x.ticks.color = color; - chart.options.scales.y.ticks.color = color; - chart.update(); + const color = theme.palette.mode === 'dark' ? 'white' : 'black'; + const gridColor = theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + chart.options.scales.x.grid.color = gridColor; + chart.options.scales.y.grid.color = gridColor; + chart.options.plugins.title.color = color; + chart.options.scales.x.ticks.color = color; + chart.options.scales.y.ticks.color = color; + chart.update(); }; useEffect(() => { From 60701aba54b1ae9b1ab12564334737db4f8a0491 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Wed, 7 Aug 2024 15:12:33 +0530 Subject: [PATCH 12/16] Explanination and Chart Time Window Selector --- src/components/Telemetry/Charts.jsx | 24 +++++++++++++++---- src/pages/Telemetry.jsx | 36 +++++++++++++---------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 3c625a19..2e06292b 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Alert, Box, Collapse, Link, useTheme } from '@mui/material'; +import { Alert, Box, Collapse, Link, MenuItem, Select, useTheme } from '@mui/material'; import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; @@ -75,6 +75,13 @@ const Charts = ({ chartSpecsText }) => { }, ]); + // New state for time window selection + const [timeWindow, setTimeWindow] = useState(60000); // default to 1 minute + + const handleTimeWindowChange = (event) => { + setTimeWindow(event.target.value); + }; + useEffect(() => { const initializeCharts = async () => { if (!chartSpecsText) { @@ -241,8 +248,7 @@ const Charts = ({ chartSpecsText }) => { x: { type: 'realtime', realtime: { - duration: reloadInterval * 4000, - delay: 3000, + duration: timeWindow, // Use timeWindow state here }, }, }, @@ -257,7 +263,7 @@ const Charts = ({ chartSpecsText }) => { }; chartLabels.forEach(createChart); - }, [chartLabels, reloadInterval]); + }, [chartLabels, reloadInterval, timeWindow]); useEffect(() => { if (Object.keys(chartsData).length === 0) { @@ -294,6 +300,16 @@ const Charts = ({ chartSpecsText }) => { {alerts.map((alert, index) => ( ))} + + {/* Time window selector */} + + + + {Object.keys(chartsData).map((path) => ( Date: Wed, 7 Aug 2024 23:53:09 +0530 Subject: [PATCH 13/16] allow to remove charts with a button --- src/components/Telemetry/Charts.jsx | 85 +++++++++++++++++------------ src/pages/Telemetry.jsx | 2 +- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 2e06292b..dafdabf0 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Alert, Box, Collapse, Link, MenuItem, Select, useTheme } from '@mui/material'; +import { Alert, Box, Button, Collapse, Link, MenuItem, Select, Tooltip, Typography, useTheme } from '@mui/material'; import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; @@ -51,7 +51,7 @@ AlertComponent.propTypes = { setAlerts: PropTypes.func.isRequired, }; -const Charts = ({ chartSpecsText }) => { +const Charts = ({ chartSpecsText, setChartSpecsText }) => { const [chartsData, setChartsData] = useState({}); const [chartLabels, setChartLabels] = useState([]); const { client: qdrantClient } = useClient(); @@ -59,6 +59,8 @@ const Charts = ({ chartSpecsText }) => { const [reloadInterval, setReloadInterval] = useState(2); const [intervalId, setIntervalId] = useState(null); const theme = useTheme(); + const [timeWindow, setTimeWindow] = useState(60000); + const [alerts, setAlerts] = useState([ { severity: 'info', @@ -75,8 +77,16 @@ const Charts = ({ chartSpecsText }) => { }, ]); - // New state for time window selection - const [timeWindow, setTimeWindow] = useState(60000); // default to 1 minute + const removeChart = (path) => { + try { + const requestBody = bigIntJSON.parse(chartSpecsText); + const newPaths = requestBody.paths.filter((p) => p !== path); + requestBody.paths = newPaths; + setChartSpecsText(JSON.stringify(requestBody, null, 2)); + } catch (e) { + console.error('Failed to remove chart', e); + } + }; const handleTimeWindowChange = (event) => { setTimeWindow(event.target.value); @@ -134,17 +144,6 @@ const Charts = ({ chartSpecsText }) => { ]); console.error('Invalid paths:', requestBody.paths); return; - } else if (requestBody.paths.length === 0) { - setAlerts((prevAlerts) => [ - ...prevAlerts, - { - severity: 'error', - message: 'No paths provided. Please provide at least one path to render charts.', - autoClose: true, - }, - ]); - console.error('No paths provided:', requestBody.paths); - return; } else if (requestBody.reload_interval && typeof requestBody.reload_interval !== 'number') { setAlerts((prevAlerts) => [ ...prevAlerts, @@ -156,12 +155,7 @@ const Charts = ({ chartSpecsText }) => { ]); console.error('Invalid reload interval:', requestBody.reload_interval); return; - } else if ( - requestBody.paths && - requestBody.paths.length > 0 && - requestBody.reload_interval && - typeof requestBody.reload_interval === 'number' - ) { + } else if (requestBody.paths && requestBody.reload_interval && typeof requestBody.reload_interval === 'number') { const fetchTelemetryData = async () => { try { const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); @@ -248,7 +242,7 @@ const Charts = ({ chartSpecsText }) => { x: { type: 'realtime', realtime: { - duration: timeWindow, // Use timeWindow state here + duration: timeWindow, }, }, }, @@ -311,18 +305,40 @@ const Charts = ({ chartSpecsText }) => { {Object.keys(chartsData).map((path) => ( - - + + + + {path.length > 50 ? ( + + {path.substring(0, 50)}... + + ) : ( + {path} + )} + + + + + + ))} @@ -331,6 +347,7 @@ const Charts = ({ chartSpecsText }) => { Charts.propTypes = { chartSpecsText: PropTypes.string.isRequired, + setChartSpecsText: PropTypes.func.isRequired, }; export default Charts; diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index 23308972..eee62293 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -63,7 +63,7 @@ function Telemetry() { overflow: 'auto', }} > - + Date: Fri, 9 Aug 2024 02:27:50 +0530 Subject: [PATCH 14/16] fix firfox bug --- src/components/Telemetry/Charts.jsx | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index dafdabf0..79a8fa56 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -159,17 +159,7 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { const fetchTelemetryData = async () => { try { const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); - requestBody.paths?.forEach((path) => { - const data = _.get(response.data.result, path, 0); - - setChartsData((prevData) => ({ - ...prevData, - [path]: { - ...prevData[path], - [new Date().toLocaleTimeString()]: data, - }, - })); - }); + setChartsData(response.data.result); } catch (error) { console.error('Failed to fetch telemetry data', error); } @@ -223,11 +213,16 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { const newChart = new Chart(context, { type: 'line', data: { - labels: Object.keys(chartsData[path]) ?? [], + labels: [Date.now()], datasets: [ { label: path, - data: Object.values(chartsData[path]) ?? [], + data: [ + { + x: Date.now(), + y: _.get(chartsData, path, 0), + }, + ], }, ], }, @@ -265,9 +260,14 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { } Object.keys(chartInstances).forEach((path) => { const chart = chartInstances[path]; - chart.data.labels = Object.keys(chartsData[path]) ?? []; - chart.data.datasets[0].data = Object.values(chartsData[path]) ?? []; - chart.update('quiet'); + const data = _.get(chartsData, path, 0); + chart.data.datasets.forEach(function (dataset) { + dataset.data.push({ + x: Date.now(), + y: data, + }); + }); + chart.update(); }); }, [chartsData]); @@ -304,7 +304,7 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { - {Object.keys(chartsData).map((path) => ( + {chartLabels.map((path) => ( Date: Fri, 9 Aug 2024 11:07:06 +0530 Subject: [PATCH 15/16] add more charts to the list instead of re-rendering all charts --- src/components/Telemetry/Charts.jsx | 121 ++++++++++++++++------------ src/pages/Telemetry.jsx | 4 +- 2 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index 79a8fa56..d54ca08e 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -54,6 +54,7 @@ AlertComponent.propTypes = { const Charts = ({ chartSpecsText, setChartSpecsText }) => { const [chartsData, setChartsData] = useState({}); const [chartLabels, setChartLabels] = useState([]); + const [telemetryData, setTelemetryData] = useState({}); const { client: qdrantClient } = useClient(); const [chartInstances, setChartInstances] = useState({}); const [reloadInterval, setReloadInterval] = useState(2); @@ -82,6 +83,7 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { const requestBody = bigIntJSON.parse(chartSpecsText); const newPaths = requestBody.paths.filter((p) => p !== path); requestBody.paths = newPaths; + setChartLabels(newPaths); setChartSpecsText(JSON.stringify(requestBody, null, 2)); } catch (e) { console.error('Failed to remove chart', e); @@ -156,10 +158,21 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { console.error('Invalid reload interval:', requestBody.reload_interval); return; } else if (requestBody.paths && requestBody.reload_interval && typeof requestBody.reload_interval === 'number') { + const paths = _.union(requestBody.paths, chartLabels); const fetchTelemetryData = async () => { try { const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); - setChartsData(response.data.result); + setTelemetryData(response.data.result); + paths?.forEach((path) => { + const data = _.get(response.data.result, path, null); + setChartsData((prevData) => ({ + ...prevData, + [path]: { + ...prevData[path], + [new Date().toLocaleTimeString()]: data, + }, + })); + }); } catch (error) { console.error('Failed to fetch telemetry data', error); } @@ -167,7 +180,7 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { await fetchTelemetryData(); - setChartLabels(requestBody.paths); + setChartLabels(paths); setReloadInterval(requestBody.reload_interval); if (requestBody.reload_interval) { @@ -197,7 +210,6 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { if (intervalId) { clearInterval(intervalId); } - setChartsData({}); setChartLabels([]); }; }, [chartSpecsText]); @@ -207,60 +219,25 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { chartInstances[chart].destroy(); }); setChartInstances({}); - - const createChart = (path) => { - const context = document.getElementById(path); - const newChart = new Chart(context, { - type: 'line', - data: { - labels: [Date.now()], - datasets: [ - { - label: path, - data: [ - { - x: Date.now(), - y: _.get(chartsData, path, 0), - }, - ], - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - mode: 'index', - intersect: false, - }, - scales: { - x: { - type: 'realtime', - realtime: { - duration: timeWindow, - }, - }, - }, - }, - }); - - syncColors(newChart); - setChartInstances((prevInstances) => ({ - ...prevInstances, - [path]: newChart, - })); - }; - chartLabels.forEach(createChart); - }, [chartLabels, reloadInterval, timeWindow]); + }, [reloadInterval, chartLabels]); + + useEffect(() => { + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + chart.options.scales.x.realtime.duration = timeWindow; + chart.update(); + }); + }, [timeWindow]); useEffect(() => { - if (Object.keys(chartsData).length === 0) { + if (Object.keys(telemetryData).length === 0) { return; } + Object.keys(chartInstances).forEach((path) => { const chart = chartInstances[path]; - const data = _.get(chartsData, path, 0); + const data = _.get(telemetryData, path, null); chart.data.datasets.forEach(function (dataset) { dataset.data.push({ x: Date.now(), @@ -269,7 +246,49 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { }); chart.update(); }); - }, [chartsData]); + }, [telemetryData]); + + const createChart = (path) => { + const context = document.getElementById(path); + const newChart = new Chart(context, { + type: 'line', + data: { + labels: Object.keys(chartsData[path] || {}), + datasets: [ + { + label: path, + data: Object.keys(chartsData[path] || {}).map((key) => ({ + x: key, + y: chartsData[path][key], + })), + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + scales: { + x: { + type: 'realtime', + realtime: { + duration: timeWindow, + }, + }, + }, + }, + }); + + syncColors(newChart); + newChart.id = path; + setChartInstances((prevInstances) => ({ + ...prevInstances, + [path]: newChart, + })); + }; const syncColors = (chart) => { const color = theme.palette.mode === 'dark' ? 'white' : 'black'; diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index eee62293..c29bdc94 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -29,11 +29,11 @@ const query = ` { "reload_interval": 2, "paths": [ + "requests.rest.responses['OPTIONS /telemetry'][200].avg_duration_micros", "app.system.disk_size", "app.system.ram_size", "collections.collections[0].shards[0].local.segments[0].info.num_indexed_vectors", - "requests.rest.responses['GET /telemetry'][200].count", - "requests.rest.responses['OPTIONS /collections'][200].count" + "requests.rest.responses['GET /telemetry'][200].count" ] }`; const defaultResult = ``; From 654f864d0b319edf71dac236d585f2e0e67f6893 Mon Sep 17 00:00:00 2001 From: kartik-gupta-ij Date: Fri, 9 Aug 2024 11:28:14 +0530 Subject: [PATCH 16/16] redesign --- src/components/Telemetry/Charts.jsx | 49 +++++++++++++++++++++++------ src/pages/Telemetry.jsx | 4 ++- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index d54ca08e..a346d682 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,6 +1,18 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Alert, Box, Button, Collapse, Link, MenuItem, Select, Tooltip, Typography, useTheme } from '@mui/material'; +import { + Alert, + Box, + Button, + Collapse, + Link, + MenuItem, + Paper, + Select, + Tooltip, + Typography, + useTheme, +} from '@mui/material'; import { bigIntJSON } from '../../common/bigIntJSON'; import { useClient } from '../../context/client-context'; import _ from 'lodash'; @@ -310,19 +322,36 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { return ( <> + + Telemetry + + Time Window: + + + {alerts.map((alert, index) => ( ))} - {/* Time window selector */} - - - - {chartLabels.map((path) => (