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: },
],
},
];