diff --git a/src/components/Telemetry/ChartControlBar.jsx b/src/components/Telemetry/ChartControlBar.jsx new file mode 100644 index 000000000..a5f6e029c --- /dev/null +++ b/src/components/Telemetry/ChartControlBar.jsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Autocomplete, + Box, + Button, + Checkbox, + Drawer, + IconButton, + ListItem, + MenuItem, + Select, + TextField, + Typography, + useMediaQuery, +} from '@mui/material'; +import { useClient } from '../../context/client-context'; +import { Close } from '@mui/icons-material'; +import TelemetryEditorWindow from './Editor'; +import { bigIntJSON } from '../../common/bigIntJSON'; + +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 ChartControlBar = ({ setChartSpecsText, timeWindow, handleTimeWindowChange, chartSpecsText }) => { + const matchesMdMedia = useMediaQuery('(max-width: 992px)'); + const [reloadInterval, setReloadInterval] = useState(2); + const [open, setOpen] = useState(false); + const [jsonPaths, setJsonPaths] = useState([]); + const [selectedPath, setSelectedPath] = useState([]); + const { client: qdrantClient } = useClient(); + const [code, setCode] = useState(query); + + function extractNumericalPaths(obj, currentPath = '') { + let paths = []; + + if (typeof obj === 'number') { + paths.push(`requests.${currentPath}`); + } else if (typeof obj === 'object' && obj !== null) { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const newPath = currentPath ? `${currentPath}.${key}` : key; + paths = [...paths, ...extractNumericalPaths(obj[key], newPath)]; + } + } + } + + return paths; + } + + useEffect(() => { + const fetchTelemetryData = async () => { + try { + const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); + setJsonPaths(extractNumericalPaths(response.data.result.requests)); + } catch (error) { + console.error('Failed to fetch telemetry data', error); + } + }; + fetchTelemetryData(); + }, []); + + const generateChartSpecs = (newSelectedPath, newReloadInterval) => { + const chartSpecs = { + reload_interval: newReloadInterval, + paths: newSelectedPath, + refresh: true, + }; + if (newReloadInterval !== reloadInterval) { + setReloadInterval(newReloadInterval); + } + if (newSelectedPath !== selectedPath) { + setSelectedPath(newSelectedPath); + } + + return JSON.stringify(chartSpecs, null, 2); + }; + + useEffect(() => { + if (chartSpecsText) { + try { + const chartSpecs = bigIntJSON.parse(chartSpecsText); + if (chartSpecs.reload_interval !== reloadInterval) { + setReloadInterval(chartSpecs.reload_interval); + } + if (chartSpecs.paths !== selectedPath) { + setSelectedPath(chartSpecs.paths); + } + } catch (e) { + console.error('Failed to parse chartSpecsText', e); + } + } + }, [chartSpecsText]); + + return ( + + ( + + )} + onChange={(event, value) => { + setChartSpecsText(generateChartSpecs(value, reloadInterval)); + }} + renderOption={(props, option, { selected }) => ( + + + + + Request: {option.split('.')[3]} + Status Code: {option.split('.')[4]} + Metric: {option.split('.')[5]} + + + + )} + /> + Reload Interval: + + Time Window: + + + setOpen(false)} + sx={{ + '& .MuiDrawer-paper': { + minWidth: matchesMdMedia ? '100vw' : '680px', + width: matchesMdMedia ? '100vw' : '55vw', + padding: '1rem', + pt: '6rem', + }, + '& .MuiBackdrop-root.MuiModal-backdrop': { + opacity: '0 !important', + }, + }} + > + + Add Charts + + + setOpen(false)}> + + + + + + + + ); +}; +ChartControlBar.propTypes = { + timeWindow: PropTypes.number.isRequired, + handleTimeWindowChange: PropTypes.func.isRequired, + setChartSpecsText: PropTypes.func.isRequired, + chartSpecsText: PropTypes.string.isRequired, +}; + +export default ChartControlBar; diff --git a/src/components/Telemetry/Charts.jsx b/src/components/Telemetry/Charts.jsx index a346d682f..c679664af 100644 --- a/src/components/Telemetry/Charts.jsx +++ b/src/components/Telemetry/Charts.jsx @@ -1,24 +1,13 @@ 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 { Alert, Box, Button, Collapse, Grid, Link, Paper, 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'; +import ChartControlBar from './ChartControlBar'; Chart.register(StreamingPlugin); @@ -69,7 +58,6 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { 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); @@ -170,7 +158,7 @@ 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 paths = requestBody.refresh ? requestBody.paths : _.union(requestBody.paths, chartLabels); const fetchTelemetryData = async () => { try { const response = await qdrantClient.api('service').telemetry({ details_level: 10 }); @@ -193,8 +181,6 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { await fetchTelemetryData(); setChartLabels(paths); - setReloadInterval(requestBody.reload_interval); - if (requestBody.reload_interval) { if (intervalId) { clearInterval(intervalId); @@ -232,7 +218,7 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { }); setChartInstances({}); chartLabels.forEach(createChart); - }, [reloadInterval, chartLabels]); + }, [chartLabels]); useEffect(() => { Object.keys(chartInstances).forEach((path) => { @@ -328,67 +314,61 @@ const Charts = ({ chartSpecsText, setChartSpecsText }) => { display: 'flex', alignItems: 'center', p: 1, - borderRadius: 0, justifyContent: 'space-between', }} > Telemetry - - Time Window: - - + {alerts.map((alert, index) => ( ))} - - {chartLabels.map((path) => ( - - - - {path.length > 50 ? ( - - {path.substring(0, 50)}... - - ) : ( - {path} - )} - - - - - - - - ))} + + {chartLabels.map((path) => ( + + + + + {path.length > 50 ? ( + + {path.substring(0, 50)}... + + ) : ( + {path} + )} + + + + + + + + + ))} + ); }; diff --git a/src/components/Telemetry/Editor.jsx b/src/components/Telemetry/Editor.jsx index d96bb951b..4a7d5d248 100644 --- a/src/components/Telemetry/Editor.jsx +++ b/src/components/Telemetry/Editor.jsx @@ -68,13 +68,26 @@ const CodeEditorWindow = ({ onChange, code, onChangeResult }) => { onChangeResult(data); }); } + + editor.onKeyDown(() => { + const isInBlockedRange = + editor.getSelections()?.findIndex((range) => new monaco.Range(0, 0, 20, 70).intersectRanges(range)) !== -1; // Block lines 1 to 3 + if (isInBlockedRange) { + editor.updateOptions({ + readOnly: true, + readOnlyMessage: { value: 'Cannot Edit the instruction' }, + }); + } + else { + editor.updateOptions({ + readOnly: false, + }); + + } + }); }); } - // function handleEditorWillMount(monaco) { - // autocomplete(monaco, qdrantClient, collectionName).then((autocomplete) => { - // autocompleteRef.current = monaco.languages.registerCompletionItemProvider('custom-language', autocomplete); - // }); - // } + return ( { + const response = await fetch(import.meta.env.BASE_URL + './openapi.json'); + const openapi = await response.json(); + + let collections = []; + try { + collections = (await qdrantClient.getCollections()).collections.map((c) => c.name); + } catch (e) { + console.error(e); + } + + const autocomplete = new OpenapiAutocomplete(openapi, collections); + + return { + provideCompletionItems: (model, position) => { + // Reuse parsed code blocks to avoid parsing the same code block multiple times + const selectedCodeBlock = monaco.editor.selectedCodeBlock; + + if (!selectedCodeBlock) { + return { suggestions: [] }; + } + + const relativeLine = position.lineNumber - selectedCodeBlock.blockStartLine; + + if (relativeLine < 0) { + // Something went wrong + return { suggestions: [] }; + } + + if (relativeLine === 0) { + // Autocomplete for request headers + const header = selectedCodeBlock.blockText.slice(0, position.column - 1); + + let suggestions = autocomplete.completeRequestHeader(header); + + suggestions = suggestions.map((s) => { + return { + label: s, + kind: 17, + insertText: s, + }; + }); + + return { suggestions: suggestions }; + } else { + // Autocomplete for request body + const requestLines = selectedCodeBlock.blockText.split(/\r?\n/); + + const lastLine = requestLines[relativeLine].slice(0, position.column - 1); + + const requestHeader = requestLines.shift(); + + const requestBodyLines = requestLines.slice(0, relativeLine - 1); + + requestBodyLines.push(lastLine); + + const requestBody = requestBodyLines.join('\n'); + + let suggestions = autocomplete.completeRequestBody(requestHeader, requestBody); + + suggestions = suggestions.map((s) => { + return { + label: s, + kind: 17, + insertText: s, + }; + }); + + return { suggestions: suggestions }; + } + }, + }; +}; diff --git a/src/pages/Telemetry.jsx b/src/pages/Telemetry.jsx index a80e3e2b7..118fcc9f6 100644 --- a/src/pages/Telemetry.jsx +++ b/src/pages/Telemetry.jsx @@ -1,49 +1,11 @@ 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 ( <> @@ -52,43 +14,11 @@ function Telemetry() { xs={12} item sx={{ - display: 'flex', - flexDirection: 'column', + overflow: 'auto', height: 'calc(100vh - 64px)', }} > - - - - - - - - - ⋮ - - - - - - +