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

Cache the detail query to avoid page crashing after refreshing #21

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export const INDICES = 'Indices';
export const SEARCH_TYPE = 'Search type';
export const NODE_ID = 'Coordinator node ID';
export const TOTAL_SHARDS = 'Total shards';
export const QUERY_DETAILS_CACHE_KEY = 'query_insights_top_queries_detail_query_key';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@elastic/elastic-eslint-config-kibana": "link:../../packages/opensearch-eslint-config-opensearch-dashboards",
"@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards",
"@testing-library/dom": "^8.11.3",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/user-event": "^14.4.3",
"@types/react-dom": "^16.9.8",
"@types/object-hash": "^3.0.0",
Expand Down
91 changes: 91 additions & 0 deletions public/pages/QueryDetails/QueryDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom';
import QueryDetails from './QueryDetails';
import Plotly from 'plotly.js-dist';
import { MockQueries } from '../../../test/testUtils';
import '@testing-library/jest-dom';
import { QUERY_DETAILS_CACHE_KEY } from '../../../common/constants';
// Mock the external dependencies
jest.mock('plotly.js-dist', () => ({
newPlot: jest.fn(),
}));

const mockCoreStart = {
chrome: {
setBreadcrumbs: jest.fn(),
},
};
const mockQuery = MockQueries()[0];
describe('QueryDetails component', () => {
beforeEach(() => {
jest.clearAllMocks(); // Clear all mock calls and instances before each test
});

it('renders QueryDetails with query from location.state', () => {
const history = createMemoryHistory();
const state = { query: mockQuery };
history.push('/query-details', state);
render(
<Router history={history}>
<QueryDetails core={mockCoreStart} />
</Router>
);
// Check if the query details are displayed correctly
expect(screen.getByText('Query details')).toBeInTheDocument();
expect(screen.getByText('Query')).toBeInTheDocument();

// Verify that the Plotly chart is rendered
expect(Plotly.newPlot).toHaveBeenCalledTimes(1);
// Verify the breadcrumbs were set correctly
expect(mockCoreStart.chrome.setBreadcrumbs).toHaveBeenCalled();
});

it('redirects to query insights if no query in state or sessionStorage', () => {
const history = createMemoryHistory();
sessionStorage.removeItem(QUERY_DETAILS_CACHE_KEY);
const pushSpy = jest.spyOn(history, 'push');
render(
<Router history={history}>
<QueryDetails core={mockCoreStart} />
</Router>
);
// Verify the redirection to QUERY_INSIGHTS when no query is found
expect(pushSpy).toHaveBeenCalledWith('/queryInsights');
});

it('retrieves query from sessionStorage if not in location.state', () => {
const history = createMemoryHistory();
// Set sessionStorage with the mock query
sessionStorage.setItem(QUERY_DETAILS_CACHE_KEY, JSON.stringify(mockQuery));
render(
<Router history={history}>
<QueryDetails core={mockCoreStart} />
</Router>
);
// Check if the query details are displayed correctly from sessionStorage
expect(screen.getByText('Query details')).toBeInTheDocument();
expect(screen.getByText('Query')).toBeInTheDocument();
// Verify that the Plotly chart is rendered
expect(Plotly.newPlot).toHaveBeenCalledTimes(1);
});

it('handles sessionStorage parsing error gracefully', () => {
const history = createMemoryHistory();
// Set sessionStorage with invalid JSON to simulate parsing error
sessionStorage.setItem(QUERY_DETAILS_CACHE_KEY, '{invalid json');
render(
<Router history={history}>
<QueryDetails core={mockCoreStart} />
</Router>
);
// Verify that the Plotly chart is not rendered due to lack of data
expect(Plotly.newPlot).not.toHaveBeenCalled();
});
});
99 changes: 64 additions & 35 deletions public/pages/QueryDetails/QueryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import Plotly from 'plotly.js-dist';
import {
EuiButton,
Expand All @@ -17,62 +17,70 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import hash from 'object-hash';
import { useParams, useHistory, useLocation } 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 { QUERY_DETAILS_CACHE_KEY } from '../../../common/constants';

const QueryDetails = ({ queries, core }: { queries: any; core: CoreStart }) => {
const { hashedQuery } = useParams<{ hashedQuery: string }>();
const query = queries.find((q: any) => hash(q) === hashedQuery);

const convertTime = (unixTime: number) => {
const date = new Date(unixTime);
const loc = date.toDateString().split(' ');
return loc[1] + ' ' + loc[2] + ', ' + loc[3] + ' @ ' + date.toLocaleTimeString('en-US');
};
interface QueryDetailsState {
query: SearchQueryRecord;
}

const QueryDetails = ({ core }: { core: CoreStart }) => {
const history = useHistory();
const location = useLocation();
const location = useLocation<QueryDetailsState>();

useEffect(() => {
core.chrome.setBreadcrumbs([
{
text: 'Query insights',
href: QUERY_INSIGHTS,
onClick: (e) => {
e.preventDefault();
history.push(QUERY_INSIGHTS);
},
},
{ text: `Query details: ${convertTime(query.timestamp)}` },
]);
}, [core.chrome, history, location, query.timestamp]);
// Get query from state or sessionStorage
const query = location.state?.query || getQueryFromSession();
function getQueryFromSession(): SearchQueryRecord | null {
try {
const cachedQuery = sessionStorage.getItem(QUERY_DETAILS_CACHE_KEY);
return cachedQuery ? JSON.parse(cachedQuery) : null;
} catch (error) {
console.error('Error reading query from sessionStorage:', error);
return null;
}
}

// Cache query if it exists
useEffect(() => {
let x: number[] = Object.values(query.phase_latency_map);
if (x.length < 3) {
x = [0, 0, 0];
if (query) {
sessionStorage.setItem(QUERY_DETAILS_CACHE_KEY, JSON.stringify(query));
} else {
// if query doesn't exist, return to overview page
history.push(QUERY_INSIGHTS);
}
}, [query, history]);

// Convert UNIX time to a readable format
const convertTime = useCallback((unixTime: number) => {
const date = new Date(unixTime);
const [_weekDay, month, day, year] = date.toDateString().split(' ');
return `${month} ${day}, ${year} @ ${date.toLocaleTimeString('en-US')}`;
}, []);

// Initialize the Plotly chart
const initPlotlyChart = useCallback(() => {
const latencies = Object.values(query?.phase_latency_map || [0, 0, 0]);
const data = [
{
x: x.reverse(),
x: latencies.reverse(),
y: ['Fetch ', 'Query ', 'Expand '],
type: 'bar',
orientation: 'h',
width: 0.5,
marker: { color: ['#F990C0', '#1BA9F5', '#7DE2D1'] },
base: [x[2] + x[1], x[2], 0],
text: x.map((value) => `${value}ms`),
base: [latencies[2] + latencies[1], latencies[2], 0],
text: latencies.map((value) => `${value}ms`),
textposition: 'outside',
cliponaxis: false,
},
];
const layout = {
autosize: true,
margin: { l: 80, r: 80, t: 25, b: 15, pad: 0 },
autorange: true,
height: 120,
xaxis: {
side: 'top',
Expand All @@ -89,7 +97,28 @@ const QueryDetails = ({ queries, core }: { queries: any; core: CoreStart }) => {
Plotly.newPlot('latency', data, layout, config);
}, [query]);

const queryString = JSON.stringify(JSON.parse(JSON.stringify(query.source)), null, 2);
useEffect(() => {
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();
}
}, [query, history, core.chrome, convertTime, initPlotlyChart]);

if (!query) {
return <div />;
}

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

return (
Expand Down Expand Up @@ -117,7 +146,7 @@ const QueryDetails = ({ queries, core }: { queries: any; core: CoreStart }) => {
target="_blank"
href="https://playground.opensearch.org/app/searchRelevance#/"
>
Open in search comparision
Open in search comparison
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
12 changes: 9 additions & 3 deletions public/pages/QueryInsights/QueryInsights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import React, { useEffect, useState } from 'react';
import { EuiBasicTableColumn, EuiInMemoryTable, EuiLink, EuiSuperDatePicker } from '@elastic/eui';
import { useHistory, useLocation } from 'react-router-dom';
import hash from 'object-hash';
import { CoreStart } from 'opensearch-dashboards/public';
import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries';
import { SearchQueryRecord } from '../../../types/types';
Expand Down Expand Up @@ -40,8 +39,8 @@
}: {
queries: SearchQueryRecord[];
loading: boolean;
onTimeChange: any;

Check warning on line 42 in public/pages/QueryInsights/QueryInsights.tsx

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected any. Specify a different type
recentlyUsedRanges: any[];

Check warning on line 43 in public/pages/QueryInsights/QueryInsights.tsx

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected any. Specify a different type
currStart: string;
currEnd: string;
core: CoreStart;
Expand Down Expand Up @@ -69,14 +68,21 @@
return `${loc[1]} ${loc[2]}, ${loc[3]} @ ${date.toLocaleTimeString('en-US')}`;
};

const cols: Array<EuiBasicTableColumn<any>> = [

Check warning on line 71 in public/pages/QueryInsights/QueryInsights.tsx

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected any. Specify a different type
{
// Make into flyout instead?
name: TIMESTAMP,
render: (query: any) => {
render: (query: SearchQueryRecord) => {
return (
<span>
<EuiLink onClick={() => history.push(`/query-details/${hash(query)}`)}>
<EuiLink
onClick={() =>
history.push({
pathname: `/query-details`,
state: { query },
})
}
>
{convertTime(query.timestamp)}
</EuiLink>
</span>
Expand All @@ -88,7 +94,7 @@
{
field: MEASUREMENTS_FIELD,
name: LATENCY,
render: (measurements: any) => {

Check warning on line 97 in public/pages/QueryInsights/QueryInsights.tsx

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected any. Specify a different type
const latencyValue = measurements?.latency?.number;
return latencyValue !== undefined ? `${latencyValue} ms` : METRIC_DEFAULT_MSG;
},
Expand Down
4 changes: 2 additions & 2 deletions public/pages/TopNQueries/TopNQueries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ const TopNQueries = ({ core }: { core: CoreStart }) => {
return (
<div style={{ padding: '35px 35px' }}>
<Switch>
<Route exact path="/query-details/:hashedQuery">
<QueryDetails queries={queries} core={core} />
<Route exact path="/query-details">
<QueryDetails core={core} />
</Route>
<Route exact path={QUERY_INSIGHTS}>
<EuiTitle size="l">
Expand Down
5 changes: 4 additions & 1 deletion public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { i18n } from '@osd/i18n';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public';
import { QueryInsightsDashboardsPluginSetup, QueryInsightsDashboardsPluginStart } from './types';
import { PLUGIN_NAME } from '../common';
import { QUERY_DETAILS_CACHE_KEY } from '../common/constants';

export class QueryInsightsDashboardsPlugin
implements Plugin<QueryInsightsDashboardsPluginSetup, QueryInsightsDashboardsPluginStart> {
Expand Down Expand Up @@ -50,5 +51,7 @@ export class QueryInsightsDashboardsPlugin
return {};
}

public stop() {}
public stop() {
sessionStorage.removeItem(QUERY_DETAILS_CACHE_KEY);
}
}
Loading
Loading