From 60d278cf842d666fce4fd99c0a82db11b7fddb7b Mon Sep 17 00:00:00 2001 From: Chenyang Ji Date: Fri, 13 Sep 2024 13:57:10 -0700 Subject: [PATCH] Cache the detail query to avoid page crashing after refreshing Signed-off-by: Chenyang Ji --- common/constants.ts | 1 + package.json | 1 + .../pages/QueryDetails/QueryDetails.test.tsx | 91 +++++++++++++++++ public/pages/QueryDetails/QueryDetails.tsx | 99 ++++++++++++------- public/pages/QueryInsights/QueryInsights.tsx | 12 ++- public/pages/TopNQueries/TopNQueries.tsx | 4 +- public/plugin.ts | 5 +- test/testUtils.ts | 99 +++++++++++++++++++ types/types.ts | 5 +- yarn.lock | 73 +++++++++++++- 10 files changed, 342 insertions(+), 48 deletions(-) create mode 100644 public/pages/QueryDetails/QueryDetails.test.tsx create mode 100644 test/testUtils.ts diff --git a/common/constants.ts b/common/constants.ts index 2186516..29309f5 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -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'; diff --git a/package.json b/package.json index 12b846f..e7312a2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/pages/QueryDetails/QueryDetails.test.tsx b/public/pages/QueryDetails/QueryDetails.test.tsx new file mode 100644 index 0000000..d71b0a5 --- /dev/null +++ b/public/pages/QueryDetails/QueryDetails.test.tsx @@ -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( + + + + ); + // 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( + + + + ); + // 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( + + + + ); + // 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( + + + + ); + // Verify that the Plotly chart is not rendered due to lack of data + expect(Plotly.newPlot).not.toHaveBeenCalled(); + }); +}); diff --git a/public/pages/QueryDetails/QueryDetails.tsx b/public/pages/QueryDetails/QueryDetails.tsx index 2da53a2..6a3cada 100644 --- a/public/pages/QueryDetails/QueryDetails.tsx +++ b/public/pages/QueryDetails/QueryDetails.tsx @@ -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, @@ -17,54 +17,63 @@ 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(); - 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, }, @@ -72,7 +81,6 @@ const QueryDetails = ({ queries, core }: { queries: any; core: CoreStart }) => { const layout = { autosize: true, margin: { l: 80, r: 80, t: 25, b: 15, pad: 0 }, - autorange: true, height: 120, xaxis: { side: 'top', @@ -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
; + } + + const queryString = JSON.stringify(query.source, null, 2); const queryDisplay = `{\n "query": ${queryString ? queryString.replace(/\n/g, '\n ') : ''}\n}`; return ( @@ -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 diff --git a/public/pages/QueryInsights/QueryInsights.tsx b/public/pages/QueryInsights/QueryInsights.tsx index e5b7e93..7ca7007 100644 --- a/public/pages/QueryInsights/QueryInsights.tsx +++ b/public/pages/QueryInsights/QueryInsights.tsx @@ -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'; @@ -73,10 +72,17 @@ const QueryInsights = ({ { // Make into flyout instead? name: TIMESTAMP, - render: (query: any) => { + render: (query: SearchQueryRecord) => { return ( - history.push(`/query-details/${hash(query)}`)}> + + history.push({ + pathname: `/query-details`, + state: { query }, + }) + } + > {convertTime(query.timestamp)} diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx index fd70c00..c6f5650 100644 --- a/public/pages/TopNQueries/TopNQueries.tsx +++ b/public/pages/TopNQueries/TopNQueries.tsx @@ -254,8 +254,8 @@ const TopNQueries = ({ core }: { core: CoreStart }) => { return (
- - + + diff --git a/public/plugin.ts b/public/plugin.ts index 9371fdf..752fc4d 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -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 { @@ -50,5 +51,7 @@ export class QueryInsightsDashboardsPlugin return {}; } - public stop() {} + public stop() { + sessionStorage.removeItem(QUERY_DETAILS_CACHE_KEY); + } } diff --git a/test/testUtils.ts b/test/testUtils.ts new file mode 100644 index 0000000..55ad71d --- /dev/null +++ b/test/testUtils.ts @@ -0,0 +1,99 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SearchQueryRecord } from '../types/types'; + +export const MockQueries = (): SearchQueryRecord[] => { + return [ + { + timestamp: 1726178995210, + task_resource_usages: [ + { + action: 'indices:data/read/search[phase/query]', + taskId: 18809, + parentTaskId: 18808, + nodeId: 'Q36D2z_NRGKim6EZZMgi6A', + taskResourceUsage: { + cpu_time_in_nanos: 3612000, + memory_in_bytes: 123944, + }, + }, + { + action: 'indices:data/read/search', + taskId: 18808, + parentTaskId: -1, + nodeId: 'Q36D2z_NRGKim6EZZMgi6A', + taskResourceUsage: { + cpu_time_in_nanos: 1898000, + memory_in_bytes: 24176, + }, + }, + ], + source: { + query: { + bool: { + must: [ + { + range: { + timestamp: { + from: 1726092595177, + to: 1726178995177, + include_lower: true, + include_upper: true, + boost: 1.0, + }, + }, + }, + ], + must_not: [ + { + match: { + indices: { + query: 'top_queries*', + operator: 'OR', + prefix_length: 0, + max_expansions: 50, + fuzzy_transpositions: true, + lenient: false, + zero_terms_query: 'NONE', + auto_generate_synonyms_phrase_query: true, + boost: 1.0, + }, + }, + }, + ], + adjust_pure_negative: true, + boost: 1.0, + }, + }, + }, + query_hashcode: '80a17984b847133b8bf5e7d5dfbfa96c', + phase_latency_map: { + expand: 0, + query: 5, + fetch: 0, + }, + labels: { + 'X-Opaque-Id': 'ae6c1170-5f98-47f4-b7fc-09ebcf574b81', + }, + total_shards: 1, + search_type: 'query_then_fetch', + node_id: 'Q36D2z_NRGKim6EZZMgi6A', + indices: ['top_queries-2024.09.12'], + measurements: { + latency: { + number: 8, + count: 1, + aggregationType: 'NONE', + }, + cpu: { + number: 5510000, + count: 1, + aggregationType: 'NONE', + }, + }, + }, + ]; +}; diff --git a/types/types.ts b/types/types.ts index 01f2f59..ca4ec71 100644 --- a/types/types.ts +++ b/types/types.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ISearchSource } from 'src/plugins/data/public'; - export interface SearchQueryRecord { timestamp: number; measurements: { @@ -14,12 +12,13 @@ export interface SearchQueryRecord { }; total_shards: number; node_id: string; - source: ISearchSource; + source: Record; labels: Record; search_type: string; indices: string[]; phase_latency_map: PhaseLatencyMap; task_resource_usages: Task[]; + query_hashcode: string; } export interface Measurement { diff --git a/yarn.lock b/yarn.lock index c5c5f59..77f03ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.0.1": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -248,7 +253,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.8" -"@babel/runtime@^7.12.5": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== @@ -735,6 +740,21 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^5.16.2": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c" + integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg== + dependencies: + "@adobe/css-tools" "^4.0.1" + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + "@testing-library/user-event@^14.4.3": version "14.5.2" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" @@ -821,7 +841,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.3.1": +"@types/jest@*", "@types/jest@^29.3.1": version "29.5.12" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== @@ -914,6 +934,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.9" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466" + integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw== + dependencies: + "@types/jest" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -1239,6 +1266,11 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" +aria-query@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.1.tgz#ebcb2c0d7fc43e68e4cb22f774d1209cb627ab42" + integrity sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1752,6 +1784,14 @@ chalk@^2.3.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -2103,6 +2143,11 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -2385,7 +2430,7 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== @@ -4526,7 +4571,7 @@ lodash.once@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.15, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4690,6 +4735,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -5353,6 +5403,14 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -5965,6 +6023,13 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"