diff --git a/public/pages/InflightQueries/InflightQueries.test.tsx b/public/pages/InflightQueries/InflightQueries.test.tsx
deleted file mode 100644
index f2903b79..00000000
--- a/public/pages/InflightQueries/InflightQueries.test.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright OpenSearch Contributors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import React from 'react';
-import { CoreStart } from 'opensearch-dashboards/public';
-import { render, screen, waitFor, act } from '@testing-library/react';
-
-import { InflightQueries } from './InflightQueries';
-import { retrieveLiveQueries } from '../../../common/utils/QueryUtils';
-jest.mock('vega-embed', () => ({
- __esModule: true,
- default: jest.fn().mockResolvedValue({}),
-}));
-import stubLiveQueries from '../../../cypress/fixtures/stub_live_queries.json';
-import '@testing-library/jest-dom';
-
-jest.mock('../../../common/utils/QueryUtils');
-
-describe('InflightQueries', () => {
- const mockCore = ({
- http: {
- get: jest.fn(),
- post: jest.fn(),
- },
- uiSettings: {
- get: jest.fn().mockReturnValue(false),
- },
- notifications: {
- toasts: {
- addSuccess: jest.fn(),
- addError: jest.fn(),
- },
- },
- } as unknown) as CoreStart;
-
- beforeEach(() => {
- jest.clearAllMocks();
- (retrieveLiveQueries as jest.Mock).mockResolvedValue(stubLiveQueries);
- });
-
- const renderInflightQueries = () => {
- return render();
- };
-
- it('displays metric values from fixture', async () => {
- renderInflightQueries();
-
- await waitFor(() => {
- expect(screen.getByText('Active queries')).toBeInTheDocument();
- expect(screen.getByText('20')).toBeInTheDocument();
- expect(screen.getByText('7.19 s')).toBeInTheDocument();
- expect(screen.getByText('9.69 s')).toBeInTheDocument();
- expect(screen.getByText('1.68 ms')).toBeInTheDocument();
- expect(screen.getByText('69.12 KB')).toBeInTheDocument();
- expect(screen.getByText('ID: node-A1B2C4E5:3614')).toBeInTheDocument();
- });
- });
-
- it('shows 0 when there are no queries', async () => {
- (retrieveLiveQueries as jest.Mock).mockResolvedValue({
- response: { live_queries: [] },
- });
-
- const { container } = renderInflightQueries();
-
- await waitFor(() => {
- expect(screen.getAllByText('0')).toHaveLength(5);
- });
- expect(container).toMatchSnapshot();
- });
-
- it('updates data periodically', async () => {
- jest.useFakeTimers();
-
- renderInflightQueries();
-
- await waitFor(() => {
- expect(retrieveLiveQueries).toHaveBeenCalledTimes(1);
- });
-
- act(() => {
- jest.advanceTimersByTime(6000);
- });
-
- await waitFor(() => {
- expect(retrieveLiveQueries).toHaveBeenCalledTimes(2);
- });
-
- act(() => {
- jest.advanceTimersByTime(6000);
- });
-
- await waitFor(() => {
- expect(retrieveLiveQueries).toHaveBeenCalledTimes(3);
- });
-
- jest.useRealTimers();
- });
-
- it('formats time values correctly', async () => {
- const mockSmallLatency = {
- response: {
- live_queries: [
- {
- id: 'query1',
- measurements: {
- latency: { number: 500 },
- cpu: { number: 1000000 },
- memory: { number: 500 },
- },
- },
- ],
- },
- };
-
- (retrieveLiveQueries as jest.Mock).mockResolvedValue(mockSmallLatency);
-
- renderInflightQueries();
-
- await waitFor(() => {
- expect(screen.getAllByText('0.50 µs')).toHaveLength(3);
- expect(screen.getAllByText('1.00 ms')).toHaveLength(2);
- });
- });
-
- it('displays fallback when live queries API fails', async () => {
- (retrieveLiveQueries as jest.Mock).mockResolvedValue({
- ok: false,
- error: 'Live queries fetch failed',
- });
-
- render();
-
- await waitFor(() => {
- expect(screen.getAllByText('Live queries fetch failed')).toHaveLength(2);
- });
- });
-});
diff --git a/public/pages/InflightQueries/InflightQueries.tsx b/public/pages/InflightQueries/InflightQueries.tsx
index 58d5beb2..a25e3325 100644
--- a/public/pages/InflightQueries/InflightQueries.tsx
+++ b/public/pages/InflightQueries/InflightQueries.tsx
@@ -18,32 +18,28 @@ import {
EuiInMemoryTable,
EuiButton,
} from '@elastic/eui';
-import embed from 'vega-embed';
-import type { VisualizationSpec } from 'vega-embed';
import { CoreStart } from 'opensearch-dashboards/public';
import { Duration } from 'luxon';
import { filesize } from 'filesize';
import { LiveSearchQueryResponse } from '../../../types/types';
import { retrieveLiveQueries } from '../../../common/utils/QueryUtils';
import { API_ENDPOINTS } from '../../../common/utils/apiendpoints';
+import { VegaChart } from './vegachart';
+import { createVegaPieSpec } from './vegaspecs/pieChartSpec';
+import { createVegaBarSpec } from './vegaspecs/barChartSpec';
export const InflightQueries = ({ core }: { core: CoreStart }) => {
const DEFAULT_REFRESH_INTERVAL = 5000; // default 5s
const TOP_N_DISPLAY_LIMIT = 9;
const isFetching = useRef(false);
const [query, setQuery] = useState(null);
- const [nodeChartError, setNodeChartError] = useState(null);
- const [indexChartError, setIndexChartError] = useState(null);
- const [nodeCounts, setNodeCounts] = useState({});
- const [indexCounts, setIndexCounts] = useState({});
+ const [nodeCounts, setNodeCounts] = useState>({});
+ const [indexCounts, setIndexCounts] = useState>({});
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true);
const [refreshInterval, setRefreshInterval] = useState(DEFAULT_REFRESH_INTERVAL);
- const chartRefByNode = useRef(null);
- const chartRefByIndex = useRef(null);
-
const liveQueries = query?.response?.live_queries ?? [];
const convertTime = (unixTime: number) => {
@@ -55,11 +51,10 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
const fetchliveQueries = async () => {
const retrievedQueries = await retrieveLiveQueries(core);
- if (retrievedQueries.error || retrievedQueries.ok === false) {
- const errorMessage = retrievedQueries.error || 'Failed to load live queries';
- setNodeChartError(errorMessage);
- setIndexChartError(errorMessage);
+ if (retrievedQueries.ok === false) {
setQuery(null);
+ setNodeCounts({});
+ setIndexCounts({});
return;
}
@@ -192,6 +187,7 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
const onChartChangeByNode = (optionId: string) => {
setSelectedChartIdByNode(optionId);
};
+
const [selectedItems, setSelectedItems] = useState([]);
const selection = {
@@ -236,101 +232,6 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
};
}, [query]);
- const getChartData = (counts: Record, type: 'node' | 'index') => {
- return Object.entries(counts).map(([key, value]) => ({
- label: type === 'node' ? `${key}` : key,
- value,
- }));
- };
-
- const getChartSpec = (type: string, chartType: 'node' | 'index'): VisualizationSpec => {
- const isDonut = type.includes('donut');
-
- return {
- width: 400,
- height: 300,
- mark: isDonut ? { type: 'arc', innerRadius: 50 } : { type: 'bar' },
- encoding: isDonut
- ? {
- theta: { field: 'value', type: 'quantitative' },
- color: {
- field: 'label',
- type: 'nominal',
- title: chartType === 'node' ? 'Nodes' : 'Indices',
- },
- tooltip: [
- { field: 'label', type: 'nominal', title: chartType === 'node' ? 'Node' : 'Index' },
- { field: 'value', type: 'quantitative', title: 'Count' },
- ],
- }
- : {
- x: {
- field: 'label',
- type: 'nominal',
- axis: { labelAngle: -45, title: chartType === 'node' ? 'Node' : 'Index' },
- },
- y: {
- field: 'value',
- type: 'quantitative',
- axis: { title: 'Count' },
- },
- color: {
- field: 'label',
- type: 'nominal',
- title: chartType === 'node' ? 'Node' : 'Index',
- },
- tooltip: [
- { field: 'label', type: 'nominal', title: chartType === 'node' ? 'Node' : 'Index' },
- { field: 'value', type: 'quantitative', title: 'Count' },
- ],
- },
- };
- };
-
- useEffect(() => {
- if (chartRefByNode.current) {
- embed(
- chartRefByNode.current,
- {
- ...getChartSpec(selectedChartIdByNode, 'node'),
- data: { values: getChartData(nodeCounts, 'node') },
- },
- { actions: false, renderer: 'svg' }
- )
- .then(() => setNodeChartError(null))
- .catch((error) => {
- console.error('Node chart rendering failed:', error);
- setNodeChartError('Failed to load chart data');
- core.notifications.toasts.addError(error, {
- title: 'Failed to render Queries by Node chart',
- toastMessage: 'Please check data or browser console for details.',
- });
- });
- }
- }, [nodeCounts, selectedChartIdByNode]);
-
- useEffect(() => {
- if (chartRefByIndex.current) {
- embed(
- chartRefByIndex.current,
- {
- ...getChartSpec(selectedChartIdByIndex, 'index'),
- data: { values: getChartData(indexCounts, 'index') },
- },
- { actions: false, renderer: 'svg' }
- )
- .then(() => setIndexChartError(null))
- .catch((error) => {
- console.error('Index chart rendering failed:', error);
- setIndexChartError('Failed to load chart data');
- core.notifications.toasts.addError(error, {
- title: 'Failed to render Queries by Index chart',
- toastMessage: 'Please check data or browser console for details.',
- });
- });
- }
- }, [indexCounts, selectedChartIdByIndex]);
-
return (
@@ -477,10 +378,10 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
+
- {/* Queries by Node */}
-
-
+
+
Queries by Node
@@ -491,38 +392,24 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
idSelected={selectedChartIdByNode}
onChange={onChartChangeByNode}
color="primary"
+ buttonSize="compressed"
/>
- {Object.keys(nodeCounts).length > 0 && !nodeChartError ? (
-
- ) : nodeChartError ? (
-
-
-
- {nodeChartError}
-
-
-
) : (
-
-
-
- No data available
-
-
-
+
)}
- {/* Queries by Index */}
-
-
+
+
Queries by Index
@@ -533,35 +420,23 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
idSelected={selectedChartIdByIndex}
onChange={onChartChangeByIndex}
color="primary"
+ buttonSize="compressed"
/>
- {Object.keys(indexCounts).length > 0 && !indexChartError ? (
-
- ) : indexChartError ? (
-
-
-
- {indexChartError}
-
-
-
) : (
-
-
-
- No data available
-
-
-
+
)}
+
{
placeholder: 'Search queries',
schema: false,
},
- toolsLeft: selectedItems.length > 0 && [
- {
- await Promise.allSettled(
- selectedItems.map((item) =>
- core.http.post(API_ENDPOINTS.CANCEL_TASK(item.id)).then(
- () => ({ status: 'fulfilled', id: item.id }),
- (err) => ({ status: 'rejected', id: item.id, error: err })
- )
- )
- );
- setSelectedItems([]);
- }}
- >
- Cancel {selectedItems.length} {selectedItems.length !== 1 ? 'queries' : 'query'}
- ,
- ],
+ toolsLeft:
+ selectedItems.length > 0
+ ? [
+ {
+ await Promise.allSettled(
+ selectedItems.map((item) =>
+ core.http.post(API_ENDPOINTS.CANCEL_TASK(item.id)).then(
+ () => ({ status: 'fulfilled', id: item.id }),
+ (err) => ({ status: 'rejected', id: item.id, error: err })
+ )
+ )
+ );
+ setSelectedItems([]);
+ }}
+ >
+ Cancel {selectedItems.length}{' '}
+ {selectedItems.length !== 1 ? 'queries' : 'query'}
+ ,
+ ]
+ : undefined,
toolsRight: [
{
{
name: 'Status',
render: (item) =>
- item.measurements?.is_cancelled === true ? (
+ item.is_cancelled === true ? (
Cancelled
@@ -681,7 +560,7 @@ export const InflightQueries = ({ core }: { core: CoreStart }) => {
icon: 'trash',
color: 'danger',
type: 'icon',
- available: (item) => item.measurements?.is_cancelled !== true,
+ available: (item) => item.is_cancelled !== true,
onClick: async (item) => {
try {
const taskId = item.id;
diff --git a/public/pages/InflightQueries/vegachart.tsx b/public/pages/InflightQueries/vegachart.tsx
new file mode 100644
index 00000000..de82b4c8
--- /dev/null
+++ b/public/pages/InflightQueries/vegachart.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useEffect, useRef } from 'react';
+import vega from 'vega';
+
+interface VegaChartProps {
+ spec: object; // Vega spec JSON
+ width?: number;
+ height?: number;
+}
+
+export const VegaChart = ({ spec, width = 400, height = 400 }: VegaChartProps) => {
+ const containerRef = useRef(null);
+ const viewRef = useRef(null);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ const runtime = vega.parse(spec);
+ const view = new vega.View(runtime)
+ .renderer('canvas')
+ .initialize(containerRef.current)
+ .width(width)
+ .height(height)
+ .run();
+
+ viewRef.current = view;
+
+ return () => {
+ if (viewRef.current) {
+ viewRef.current.finalize();
+ viewRef.current = null;
+ }
+ };
+ }, [spec, width, height]);
+
+ return (
+
+ );
+};
diff --git a/public/pages/InflightQueries/vegaspecs/barChartSpec.ts b/public/pages/InflightQueries/vegaspecs/barChartSpec.ts
new file mode 100644
index 00000000..6b69c2d2
--- /dev/null
+++ b/public/pages/InflightQueries/vegaspecs/barChartSpec.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const createVegaBarSpec = (data: Record, width = 100, height = 200) => {
+ const values = Object.entries(data).map(([category, value]) => ({ category, value }));
+
+ return {
+ $schema: 'https://vega.github.io/schema/vega/v5.json',
+ width,
+ height,
+ padding: 5,
+ autosize: 'pad',
+ data: [
+ {
+ name: 'table',
+ values,
+ },
+ ],
+ scales: [
+ {
+ name: 'yscale',
+ type: 'band',
+ domain: { data: 'table', field: 'category' },
+ range: 'height',
+ padding: 0.05,
+ round: true,
+ },
+ {
+ name: 'xscale',
+ domain: { data: 'table', field: 'value' },
+ nice: true,
+ range: 'width',
+ },
+ {
+ name: 'color',
+ type: 'ordinal',
+ domain: { data: 'table', field: 'category' },
+ range: { scheme: 'category20' },
+ },
+ ],
+ axes: [
+ { orient: 'left', scale: 'yscale' },
+ { orient: 'bottom', scale: 'xscale' },
+ ],
+ marks: [
+ {
+ type: 'rect',
+ from: { data: 'table' },
+ encode: {
+ enter: {
+ y: { scale: 'yscale', field: 'category' },
+ height: { scale: 'yscale', band: 1 },
+ x: { scale: 'xscale', value: 0 },
+ x2: { scale: 'xscale', field: 'value' },
+ fill: { scale: 'color', field: 'category' },
+ },
+ },
+ },
+ ],
+ legends: [
+ {
+ fill: 'color',
+ orient: 'bottom',
+ title: 'Category',
+ columns: 3, // number of columns for legend items
+ columnPadding: 10, // optional spacing between columns
+ encode: {
+ labels: { update: { fontSize: { value: 16 } } },
+ symbols: { update: { size: { value: 100 } } },
+ },
+ },
+ ],
+ };
+};
diff --git a/public/pages/InflightQueries/vegaspecs/pieChartSpec.ts b/public/pages/InflightQueries/vegaspecs/pieChartSpec.ts
new file mode 100644
index 00000000..7de36f7b
--- /dev/null
+++ b/public/pages/InflightQueries/vegaspecs/pieChartSpec.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const createVegaPieSpec = (data: Record, width = 350, height = 350) => {
+ const values = Object.entries(data).map(([category, value]) => ({ category, value }));
+
+ return {
+ $schema: 'https://vega.github.io/schema/vega/v5.json',
+ width,
+ height: height + 100,
+ padding: { top: 10, left: 10, right: 10, bottom: 100 },
+ autosize: 'pad',
+ signals: [
+ { name: 'startAngle', value: 0 },
+ { name: 'endAngle', value: 6.29 },
+ { name: 'padAngle', value: 0 },
+ { name: 'innerRadius', value: 50 },
+ { name: 'cornerRadius', value: 0 },
+ { name: 'sort', value: false },
+ ],
+ data: [
+ {
+ name: 'table',
+ values,
+ transform: [
+ {
+ type: 'pie',
+ field: 'value',
+ startAngle: { signal: 'startAngle' },
+ endAngle: { signal: 'endAngle' },
+ padAngle: { signal: 'padAngle' },
+ sort: { signal: 'sort' },
+ },
+ ],
+ },
+ ],
+ scales: [
+ {
+ name: 'color',
+ type: 'ordinal',
+ domain: { data: 'table', field: 'category' },
+ range: { scheme: 'category20' },
+ },
+ ],
+ marks: [
+ {
+ type: 'arc',
+ from: { data: 'table' },
+ encode: {
+ enter: {
+ fill: { scale: 'color', field: 'category' },
+ x: { signal: 'width / 2' },
+ y: { signal: '(height - 100) / 2' },
+ startAngle: { field: 'startAngle' },
+ endAngle: { field: 'endAngle' },
+ innerRadius: { signal: 'innerRadius' },
+ outerRadius: { signal: 'min(width, height) / 2 - 50' },
+ },
+ update: {
+ cornerRadius: { signal: 'cornerRadius' },
+ padAngle: { signal: 'padAngle' },
+ },
+ },
+ },
+ ],
+ legends: [
+ {
+ fill: 'color',
+ orient: 'bottom',
+ title: 'Category',
+ columns: 3, // number of columns for legend items
+ columnPadding: 10, // optional spacing between columns
+ encode: {
+ labels: { update: { fontSize: { value: 16 } } },
+ symbols: { update: { size: { value: 100 } } },
+ },
+ },
+ ],
+ };
+};