Skip to content

Commit

Permalink
Cache the detail query to avoid page crashing after refreshing
Browse files Browse the repository at this point in the history
Signed-off-by: Chenyang Ji <[email protected]>
  • Loading branch information
ansjcy committed Sep 13, 2024
1 parent dcda4b8 commit 8ac3e45
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 48 deletions.
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();
});
});
98 changes: 63 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,69 @@ 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 {
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 +96,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 +145,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 @@ -73,10 +72,17 @@ const QueryInsights = ({
{
// 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 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

0 comments on commit 8ac3e45

Please sign in to comment.