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/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/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx new file mode 100644 index 00000000..a346d682 --- /dev/null +++ b/src/components/Telemetry/Charts.jsx @@ -0,0 +1,401 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +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'; +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) { + const timer = setTimeout(() => { + setOpen(false); + }, 5000); + + return () => clearTimeout(timer); + } + }, [alert.autoClose]); + + return ( + { + if (!open) { + setAlerts((prevAlerts) => prevAlerts.filter((_, i) => i !== index)); + } + }} + > + { + setOpen(false); + }} + severity={alert.severity} + > + {alert.message} + + + ); +}; + +AlertComponent.propTypes = { + alert: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, + setAlerts: PropTypes.func.isRequired, +}; + +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); + const [intervalId, setIntervalId] = useState(null); + const theme = useTheme(); + const [timeWindow, setTimeWindow] = useState(60000); + + 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, + }, + ]); + + const removeChart = (path) => { + try { + 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); + } + }; + + const handleTimeWindowChange = (event) => { + setTimeWindow(event.target.value); + }; + + useEffect(() => { + 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; + } + + let requestBody; + + 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) { + 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.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.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 }); + 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); + } + }; + + await fetchTelemetryData(); + + setChartLabels(paths); + setReloadInterval(requestBody.reload_interval); + + if (requestBody.reload_interval) { + if (intervalId) { + clearInterval(intervalId); + } + const newIntervalId = setInterval(fetchTelemetryData, requestBody.reload_interval * 1000); + setIntervalId(newIntervalId); + } + } 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(); + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + setChartLabels([]); + }; + }, [chartSpecsText]); + + useEffect(() => { + Object.keys(chartInstances).forEach((chart) => { + chartInstances[chart].destroy(); + }); + setChartInstances({}); + chartLabels.forEach(createChart); + }, [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(telemetryData).length === 0) { + return; + } + + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + const data = _.get(telemetryData, path, null); + chart.data.datasets.forEach(function (dataset) { + dataset.data.push({ + x: Date.now(), + y: data, + }); + }); + chart.update(); + }); + }, [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'; + 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(() => { + Object.keys(chartInstances).forEach((path) => { + const chart = chartInstances[path]; + syncColors(chart); + }); + }, [theme.palette.mode]); + + return ( + <> + + Telemetry + + Time Window: + + + + {alerts.map((alert, index) => ( + + ))} + + {chartLabels.map((path) => ( + + + + {path.length > 50 ? ( + + {path.substring(0, 50)}... + + ) : ( + {path} + )} + + + + + + + + ))} + + ); +}; + +Charts.propTypes = { + chartSpecsText: PropTypes.string.isRequired, + setChartSpecsText: PropTypes.func.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 new file mode 100644 index 00000000..a80e3e2b --- /dev/null +++ b/src/pages/Telemetry.jsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { Box, Grid } 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 = ` +// Graph Rendering: +// Graphs are rendered based on numerical outputs obtained from the specified JSON paths. +// Only metrics that return numeric values are used to create visual representations. +// This ensures that the graphs are meaningful and accurately reflect the system's status. + +// JSON Path: +// The JSON paths listed here are extracted from the /telemetry?details_level=10 endpoint. +// They represent specific fields within the JSON response structure returned by the API. +// For example, paths like 'collections.collections[0].shards[0].local.segments[0].info.num_indexed_vectors' +// indicate nested structures where specific values are accessed using indices or keys. +// This path extracts the number of indexed vectors in the first segment of the first shard of the first collection, +// which is crucial for understanding the indexing performance of the collection. + +// Reload Interval: +// The reload_interval is set to 2 seconds by default. +// This means that the graphs and data metrics on the page will be updated every 2 seconds, +// providing near real-time monitoring of the system's performance. + +//Time Window: +// The time window for the graphs is set to 1 min by default. + + +{ + "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" + ] +}`; +const defaultResult = ``; +function Telemetry() { + const [code, setCode] = useState(query); + const [result, setResult] = useState(defaultResult); + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + ⋮ + + + + + + + + + + + ); +} + +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: }, ], }, ];