Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query/group details make API call to fetch data #63

Merged
merged 1 commit into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion public/pages/QueryDetails/Components/QuerySummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@ const PanelItem = ({ label, value }: { label: string; value: string | number })
</EuiFlexItem>
);

const QuerySummary = ({ query }: { query: SearchQueryRecord }) => {
const QuerySummary = ({ query }: { query: SearchQueryRecord | null }) => {
// If query is null, return a message indicating no data is available
if (!query) {
return (
<EuiPanel data-test-subj={'query-details-summary-section'}>
<EuiText size="xs">
<h2>No Data Available</h2>
deshsidd marked this conversation as resolved.
Show resolved Hide resolved
</EuiText>
</EuiPanel>
);
}

const convertTime = (unixTime: number) => {
const date = new Date(unixTime);
const loc = date.toDateString().split(' ');
Expand Down
124 changes: 96 additions & 28 deletions public/pages/QueryDetails/QueryDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import QueryDetails from './QueryDetails';
import Plotly from 'plotly.js-dist';
import { MockQueries } from '../../../test/testUtils';
import '@testing-library/jest-dom';
// @ts-ignore
import plotly from 'plotly.js-dist';
import { MemoryRouter, Route } from 'react-router-dom';
import hash from 'object-hash';
// Mock the external dependencies
import { retrieveQueryById } from '../Utils/QueryUtils';

jest.mock('plotly.js-dist', () => ({
newPlot: jest.fn(),
}));

jest.mock('../Utils/QueryUtils', () => ({
retrieveQueryById: jest.fn(),
}));

const mockCoreStart = {
chrome: {
setBreadcrumbs: jest.fn(),
Expand All @@ -24,42 +30,104 @@ const mockCoreStart = {
get: jest.fn().mockReturnValue(false),
},
};

const mockQuery = MockQueries()[0];
const mockParams = { hashedQuery: hash(mockQuery) };

describe('QueryDetails component', () => {
beforeAll(() => {
jest.spyOn(Date.prototype, 'toLocaleTimeString').mockImplementation(() => '12:00:00 AM');
jest.spyOn(Date.prototype, 'toDateString').mockImplementation(() => 'Mon Jan 13 2025');
});

afterAll(() => {
jest.resetAllMocks(); // Reset all mocks after all tests
});

beforeEach(() => {
jest.clearAllMocks(); // Clear all mock calls and instances before each test
jest.clearAllMocks();
(retrieveQueryById as jest.Mock).mockResolvedValue(mockQuery);
});

const renderComponent = () =>
render(
<MemoryRouter initialEntries={[`/query-details/${mockParams.hashedQuery}`]}>
<Route path="/query-details/:hashedQuery">
<QueryDetails queries={MockQueries()} core={mockCoreStart} />
const renderQueryDetails = () => {
return render(
<MemoryRouter
initialEntries={[
`/query-details/?id=${hash(
mockQuery.id
)}&from=2025-01-21T22:30:33.347Z&to=2025-01-22T22:30:33.347Z`,
]}
>
<Route path="/query-details">
<QueryDetails core={mockCoreStart} />
</Route>
</MemoryRouter>
);
};

it('renders the main components', async () => {
renderQueryDetails();

await waitFor(() => {
expect(screen.getByText('Query details')).toBeInTheDocument();
expect(screen.getByText('Query')).toBeInTheDocument();
expect(screen.getByText('Latency')).toBeInTheDocument();
});
});

const getByTestSubj = (container: HTMLElement, id: string) =>
container.querySelector(`[data-test-subj="${id}"]`);

it('fetches and displays query data', async () => {
const { container } = renderQueryDetails();

await waitFor(() => {
expect(retrieveQueryById).toHaveBeenCalled();
});

const sourceSection = getByTestSubj(container, 'query-details-source-section');
const latencyChart = getByTestSubj(container, 'query-details-latency-chart');

expect(sourceSection).toBeInTheDocument();
expect(latencyChart).toBeInTheDocument();
});

it('initializes the Plotly chart', async () => {
renderQueryDetails();

await waitFor(() => {
expect(plotly.newPlot).toHaveBeenCalled();
expect(plotly.newPlot.mock.calls[0][0]).toBe('latency');
});
});

it('sets breadcrumbs correctly', async () => {
renderQueryDetails();

await waitFor(() => {
expect(mockCoreStart.chrome.setBreadcrumbs).toHaveBeenCalled();
const breadcrumbs = mockCoreStart.chrome.setBreadcrumbs.mock.calls[0][0];
expect(breadcrumbs[0].text).toBe('Query insights');
expect(breadcrumbs[1].text).toContain('Query details:');
});
});

it('renders the search comparison button', async () => {
renderQueryDetails();

await waitFor(() => {
const button = screen.getByText('Open in search comparison');
expect(button).toBeInTheDocument();
expect(button.closest('a')).toHaveAttribute(
'href',
'https://playground.opensearch.org/app/searchRelevance#/'
);
});
});

it('matches snapshot', async () => {
const { container } = renderQueryDetails();

it('renders the QueryDetails page', () => {
const { container } = renderComponent();
// Check if the query details are displayed correctly
expect(screen.getByText('Query details')).toBeInTheDocument();
expect(screen.getByText('Query')).toBeInTheDocument();
await waitFor(() => {
expect(retrieveQueryById).toHaveBeenCalled();
});

// Verify that the Plotly chart is rendered
expect(Plotly.newPlot).toHaveBeenCalledTimes(1);
// Verify the breadcrumbs were set correctly
expect(mockCoreStart.chrome.setBreadcrumbs).toHaveBeenCalled();
const dateElements = container.getElementsByClassName('euiText euiText--extraSmall');
Array.from(dateElements).forEach((element) => {
if (element.textContent?.includes('@')) {
element.textContent = 'Sep 24, 2021 @ 12:00:00 AM';
}
});

expect(container).toMatchSnapshot();
});
Expand Down
58 changes: 39 additions & 19 deletions public/pages/QueryDetails/QueryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
// @ts-ignore
import Plotly from 'plotly.js-dist';
import {
EuiButton,
Expand All @@ -17,26 +18,30 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import hash from 'object-hash';
import { useParams, useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { CoreStart } from 'opensearch-dashboards/public';
import QuerySummary from './Components/QuerySummary';
import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries';
import { SearchQueryRecord } from '../../../types/types';
import { PageHeader } from '../../components/PageHeader';
import { QueryInsightsDashboardsPluginStartDependencies } from '../../types';
import { retrieveQueryById } from '../Utils/QueryUtils';

const QueryDetails = ({
queries,
core,
depsStart,
}: {
queries: any;
core: CoreStart;
depsStart: QueryInsightsDashboardsPluginStartDependencies;
}) => {
const { hashedQuery } = useParams<{ hashedQuery: string }>();
const query = queries.find((q: SearchQueryRecord) => hash(q) === hashedQuery);
// Get url parameters
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const id = searchParams.get('id');
const from = searchParams.get('from');
const to = searchParams.get('to');

const [query, setQuery] = useState<SearchQueryRecord | null>(null);
const history = useHistory();

// Convert UNIX time to a readable format
Expand All @@ -46,6 +51,17 @@ const QueryDetails = ({
return `${month} ${day}, ${year} @ ${date.toLocaleTimeString('en-US')}`;
}, []);

useEffect(() => {
const fetchQueryDetails = async () => {
const retrievedQuery = await retrieveQueryById(core, from, to, id);
setQuery(retrievedQuery);
};

if (id && from && to) {
deshsidd marked this conversation as resolved.
Show resolved Hide resolved
fetchQueryDetails();
}
}, [id, from, to]);

// Initialize the Plotly chart
const initPlotlyChart = useCallback(() => {
const latencies: number[] = Object.values(query?.phase_latency_map || [0, 0, 0]);
Expand Down Expand Up @@ -83,21 +99,25 @@ const QueryDetails = ({
}, [query]);

useEffect(() => {
core.chrome.setBreadcrumbs([
{
text: 'Query insights',
href: QUERY_INSIGHTS,
onClick: (e) => {
e.preventDefault();
history.push(QUERY_INSIGHTS);
if (query) {
core.chrome.setBreadcrumbs([
{
text: 'Query insights',
href: QUERY_INSIGHTS,
onClick: (e) => {
e.preventDefault();
history.push(QUERY_INSIGHTS);
},
},
},
{ text: `Query details: ${convertTime(query.timestamp)}` },
]);
initPlotlyChart();
{ text: `Query details: ${convertTime(query.timestamp)}` },
]);
initPlotlyChart();
}
}, [query, history, core.chrome, convertTime, initPlotlyChart]);

const queryString = JSON.stringify(query.source, null, 2);
const queryString = query
? JSON.stringify(JSON.parse(JSON.stringify(query.source)), null, 2)
: '';
const queryDisplay = `{\n "query": ${queryString ? queryString.replace(/\n/g, '\n ') : ''}\n}`;

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`QueryDetails component renders the QueryDetails page 1`] = `
exports[`QueryDetails component matches snapshot 1`] = `
<div>
<div>
<h1
Expand Down Expand Up @@ -44,7 +44,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
<div
class="euiText euiText--extraSmall"
>
Jan 13, 2025 @ 12:00:00 AM
Sep 24, 2021 @ 12:00:00 AM
</div>
</div>
<div
Expand Down Expand Up @@ -202,7 +202,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon--inherit euiIcon-isLoading euiButtonContent__icon"
class="euiIcon euiIcon--medium euiIcon--inherit euiButtonContent__icon"
focusable="false"
height="16"
role="img"
Expand All @@ -211,7 +211,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.277 10.088c.02.014.04.03.057.047.582.55 1.134.812 1.666.812.586 0 1.84-.293 3.713-.88L9 6.212V2H7v4.212l-1.723 3.876Zm-.438.987L3.539 14h8.922l-1.32-2.969C9.096 11.677 7.733 12 7 12c-.74 0-1.463-.315-2.161-.925ZM6 2H5V1h6v1h-1v4l3.375 7.594A1 1 0 0 1 12.461 15H3.54a1 1 0 0 1-.914-1.406L6 6V2Z"
d="M13 8.5a.5.5 0 1 1 1 0V12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3.5a.5.5 0 0 1 0 1H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8.5Zm-5.12.339a.5.5 0 1 1-.706-.707L13.305 2H10.5a.5.5 0 1 1 0-1H14a1 1 0 0 1 1 1v3.5a.5.5 0 1 1-1 0V2.72L7.88 8.838Z"
/>
</svg>
<span
Expand Down Expand Up @@ -1080,7 +1080,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon--inherit euiIcon-isLoading euiButtonIcon__icon"
class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon"
focusable="false"
height="16"
role="img"
Expand All @@ -1089,7 +1089,8 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.277 10.088c.02.014.04.03.057.047.582.55 1.134.812 1.666.812.586 0 1.84-.293 3.713-.88L9 6.212V2H7v4.212l-1.723 3.876Zm-.438.987L3.539 14h8.922l-1.32-2.969C9.096 11.677 7.733 12 7 12c-.74 0-1.463-.315-2.161-.925ZM6 2H5V1h6v1h-1v4l3.375 7.594A1 1 0 0 1 12.461 15H3.54a1 1 0 0 1-.914-1.406L6 6V2Z"
d="M13 3v4h-1V4H9V3h4ZM3 3h4v1H4v3H3V3Zm10 10H9v-1h3V9h1v4ZM3 13V9h1v3h3v1H3ZM0 1.994C0 .893.895 0 1.994 0h12.012C15.107 0 16 .895 16 1.994v12.012A1.995 1.995 0 0 1 14.006 16H1.994A1.995 1.995 0 0 1 0 14.006V1.994Zm1 0v12.012c0 .548.446.994.994.994h12.012a.995.995 0 0 0 .994-.994V1.994A.995.995 0 0 0 14.006 1H1.994A.995.995 0 0 0 1 1.994Z"
fill-rule="evenodd"
/>
</svg>
</button>
Expand All @@ -1106,7 +1107,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon--inherit euiIcon-isLoading euiButtonIcon__icon"
class="euiIcon euiIcon--medium euiIcon--inherit euiButtonIcon__icon"
focusable="false"
height="16"
role="img"
Expand All @@ -1115,7 +1116,10 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.277 10.088c.02.014.04.03.057.047.582.55 1.134.812 1.666.812.586 0 1.84-.293 3.713-.88L9 6.212V2H7v4.212l-1.723 3.876Zm-.438.987L3.539 14h8.922l-1.32-2.969C9.096 11.677 7.733 12 7 12c-.74 0-1.463-.315-2.161-.925ZM6 2H5V1h6v1h-1v4l3.375 7.594A1 1 0 0 1 12.461 15H3.54a1 1 0 0 1-.914-1.406L6 6V2Z"
d="M11.4 0c.235 0 .46.099.622.273l2.743 3c.151.162.235.378.235.602v9.25a.867.867 0 0 1-.857.875H3.857A.867.867 0 0 1 3 13.125V.875C3 .392 3.384 0 3.857 0H11.4ZM14 4h-2.6a.4.4 0 0 1-.4-.4V1H4v12h10V4Z"
/>
<path
d="M3 1H2a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-1h-1v1H2V2h1V1Z"
/>
</svg>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const PanelItem = ({ label, value }: { label: string; value: string | number })
);

export const QueryGroupAggregateSummary = ({ query }: { query: any }) => {
if (!query) {
return <EuiText size="s">No query data available.</EuiText>;
deshsidd marked this conversation as resolved.
Show resolved Hide resolved
}
const { measurements, id: id, group_by: groupBy } = query;
const queryCount =
measurements.latency?.count || measurements.cpu?.count || measurements.memory?.count || 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const PanelItem = ({ label, value }: { label: string; value: string | number })
);

export const QueryGroupSampleQuerySummary = ({ query }: { query: any }) => {
if (!query) {
return <EuiText size="s">No query data available.</EuiText>;
}
const convertTime = (unixTime: number) => {
const date = new Date(unixTime);
const loc = date.toDateString().split(' ');
Expand Down
Loading
Loading