diff --git a/.cypress/integration/10_datasources.spec.js b/.cypress/integration/10_datasources.spec.js new file mode 100644 index 000000000..fdfe21b28 --- /dev/null +++ b/.cypress/integration/10_datasources.spec.js @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + + + const moveToDatasourcesHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/datasources`); + }; + + const moveToNewDatasourcesPage = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/datasources#/new`); + }; + + const moveToCreatePrometheusDatasourcePage = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/datasources#/configure/PROMETHEUS`); + }; + + describe('Integration tests for datasources plugin', () => { + const testPrometheusSuffix = (Math.random() + 1).toString(36).substring(7); +const testPrometheusInstance = `Prometheus_${testPrometheusSuffix}`; +const testS3Suffix = (Math.random() + 1).toString(36).substring(7); +const testS3Instance = `S3_${testS3Suffix}`; + it('Navigates to datasources plugin and expects the correct header', () => { + moveToDatasourcesHome(); + cy.get('[data-test-subj="dataconnections-header"]').should('exist'); + }); + + it('Tests navigation between tabs and goes to Prometheus creation flow', () => { + moveToDatasourcesHome(); + cy.get('[data-test-subj="new"]').click(); + cy.url().should('include', '/new') + cy.get('[data-test-subj="datasource_card_prometheus"]').click(); + cy.url().should('include', '/configure/PROMETHEUS'); + }); + +}); + + + diff --git a/.cypress/integration/1_event_analytics.spec.js b/.cypress/integration/1_event_analytics.spec.js index cbbeeae47..f25987f4f 100644 --- a/.cypress/integration/1_event_analytics.spec.js +++ b/.cypress/integration/1_event_analytics.spec.js @@ -108,7 +108,8 @@ describe('Open flyout for a data row to see details', () => { }); }); -describe('Add/delete/switch explorer top level tabs', () => { +// skip for now due to tab removals +describe.skip('Add/delete/switch explorer top level tabs', () => { beforeEach(() => { landOnEventExplorer(); }); @@ -406,7 +407,7 @@ describe('Live tail stop automatically', () => { landOnEventExplorer(); }); - it('Moving to other tab should stop live tail automatically', () => { + it.skip('Moving to other tab should stop live tail automatically', () => { clearQuerySearchBoxText('searchAutocompleteTextArea'); cy.get('[data-test-subj="searchAutocompleteTextArea"]').type(TEST_QUERIES[1].query); cy.get('[data-test-subj=eventLiveTail]').click(); @@ -414,7 +415,7 @@ describe('Live tail stop automatically', () => { cy.get('.euiToastHeader__title').contains('On').should('exist'); }); - it('Add a new tab', () => { + it.skip('Add a new tab', () => { cy.get('[data-test-subj="eventExplorer__topLevelTabbing"]') .find('button.euiTab') .then((lists) => { @@ -426,7 +427,7 @@ describe('Live tail stop automatically', () => { }); }); - it('Click to switch to another tab', () => { + it.skip('Click to switch to another tab', () => { cy.get('[data-test-subj="eventExplorer__addNewTab"]', { timeout: COMMAND_TIMEOUT_LONG, }).click(); @@ -441,7 +442,7 @@ describe('Live tail stop automatically', () => { .should('have.class', 'euiTab-isSelected'); }); - it('Close current selected tab', () => { + it.skip('Close current selected tab', () => { cy.get('[data-test-subj="eventExplorer__addNewTab"]', { timeout: COMMAND_TIMEOUT_LONG, }).click(); diff --git a/.github/workflows/dashboards-observability-test-and-build-workflow.yml b/.github/workflows/dashboards-observability-test-and-build-workflow.yml index 8093d8fd5..5e91e0bbe 100644 --- a/.github/workflows/dashboards-observability-test-and-build-workflow.yml +++ b/.github/workflows/dashboards-observability-test-and-build-workflow.yml @@ -51,11 +51,15 @@ jobs: with: path: OpenSearch-Dashboards/plugins/dashboards-observability - - name: Plugin Bootstrap - run: | - cd OpenSearch-Dashboards/plugins/dashboards-observability - yarn osd bootstrap + uses: nick-fields/retry@v2 + with: + timeout_minutes: 20 + max_attempts: 2 + command: | + cd OpenSearch-Dashboards + yarn config set network-timeout 1000000 -g + yarn osd bootstrap - name: Test all dashboards-observability modules run: | diff --git a/auto_sync_commit_metadata.json b/auto_sync_commit_metadata.json index 81d355c0b..d5abbb51f 100644 --- a/auto_sync_commit_metadata.json +++ b/auto_sync_commit_metadata.json @@ -1,4 +1,4 @@ { - "last_github_commit": "cefd57fd94ddc916603128ae2cf7cf18c5962b38", - "last_gitfarm_commit": "3905d90c37023bc6d6e0a3508a46f8e99d353836" + "last_github_commit": "171acde2cd2468d619cdd9bd7051144197ebd89d", + "last_gitfarm_commit": "4b5220261e4f5ecf259856ff1963e8e29dd1644b" } \ No newline at end of file diff --git a/common/constants/data_connections.ts b/common/constants/data_connections.ts index 41520976e..6d77f8d72 100644 --- a/common/constants/data_connections.ts +++ b/common/constants/data_connections.ts @@ -7,8 +7,6 @@ import { DatasourceType } from '../../common/types/data_connections'; export const OPENSEARCH_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/data-sources/index'; -export const OPENSEARCH_ACC_DOCUMENTATION_URL = - 'https://opensearch.org/docs/latest/data-acceleration/index'; export const QUERY_RESTRICTED = 'query-restricted'; export const QUERY_ALL = 'query-all'; diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index 6b01e8c34..c0942c0a1 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -7,10 +7,13 @@ import { htmlIdGenerator } from '@elastic/eui'; import { ThresholdUnitType } from '../../public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds'; import { VIS_CHART_TYPES } from './shared'; +// URLs export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/event-analytics/'; export const OPEN_TELEMETRY_LOG_CORRELATION_LINK = 'https://opentelemetry.io/docs/reference/specification/logs/overview/#log-correlation'; +export const LOG_EXPLORER_BASE_PATH = 'observability-logs#/explorer/'; + export const RAW_QUERY = 'rawQuery'; export const FINAL_QUERY = 'finalQuery'; export const SELECTED_DATE_RANGE = 'selectedDateRange'; @@ -50,6 +53,8 @@ export const DEFAULT_COLUMNS = ['', 'Time', '_source']; export const OTEL_TRACE_ID = 'traceId'; export const JAEGER_TRACE_ID = 'traceID'; export const DATE_PICKER_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +export const DATE_DISPLAY_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS'; +export const DEFAULT_DATETIME_STRING = 'now'; export const TIME_INTERVAL_OPTIONS = [ { text: 'Minute', @@ -320,3 +325,23 @@ export const TYPE_TAB_MAPPING = { [SAVED_QUERY]: TAB_EVENT_ID, [SAVED_VISUALIZATION]: TAB_CHART_ID, }; + +export const DEFAULT_EMPTY_EXPLORER_FIELDS = [ + { name: 'timestamp', type: 'timestamp' }, + { name: '_source', type: 'string' }, +]; + +export const DEFAULT_TIMESTAMP_COLUMN = { + id: 'timestamp', + isSortable: true, + display: 'Time', + schema: 'datetime', + initialWidth: 200, +}; + +export const DEFAULT_SOURCE_COLUMN = { + id: '_source', + isSortable: false, + display: 'Source', + schema: '_source', +}; diff --git a/common/constants/integrations.ts b/common/constants/integrations.ts index 9df90f004..2d31c3c0f 100644 --- a/common/constants/integrations.ts +++ b/common/constants/integrations.ts @@ -5,3 +5,7 @@ export const OPENSEARCH_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/integrations/index'; export const ASSET_FILTER_OPTIONS = ['index-pattern', 'search', 'visualization', 'dashboard']; +export const VALID_INDEX_NAME = /^[a-z\d\.][a-z\d\._\-\*]*$/; + +// Upstream doesn't export this, so we need to redeclare it for our use. +export type Color = 'success' | 'primary' | 'warning' | 'danger' | undefined; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index c8e47aa87..ddcffeb30 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ import CSS from 'csstype'; -import { IField } from '../../common/types/explorer'; // Client route export const PPL_BASE = '/api/ppl'; @@ -14,20 +13,18 @@ export const DSL_CAT = '/cat.indices'; export const DSL_MAPPING = '/indices.getFieldMapping'; export const OBSERVABILITY_BASE = '/api/observability'; export const INTEGRATIONS_BASE = '/api/integrations'; -export const JOBS_BASE = '/query/jobs'; export const DATACONNECTIONS_BASE = '/api/dataconnections'; export const EVENT_ANALYTICS = '/event_analytics'; export const SAVED_OBJECTS = '/saved_objects'; export const SAVED_QUERY = '/query'; export const SAVED_VISUALIZATION = '/vis'; +export const CONSOLE_PROXY = '/api/console/proxy'; // Server route export const PPL_ENDPOINT = '/_plugins/_ppl'; export const SQL_ENDPOINT = '/_plugins/_sql'; export const DSL_ENDPOINT = '/_plugins/_dsl'; export const DATACONNECTIONS_ENDPOINT = '/_plugins/_query/_datasources'; -export const JOBS_ENDPOINT_BASE = '/_plugins/_async_query'; -export const JOB_RESULT_ENDPOINT = '/result'; export const observabilityID = 'observability-logs'; export const observabilityTitle = 'Observability'; @@ -61,6 +58,12 @@ export const observabilityIntegrationsID = 'integrations'; export const observabilityIntegrationsTitle = 'Integrations'; export const observabilityIntegrationsPluginOrder = 9020; +export const observabilityDataConnectionsID = 'datasources'; +export const observabilityDataConnectionsTitle = 'Data sources'; +export const observabilityDataConnectionsPluginOrder = 9030; + +export const queryWorkbenchPluginID = 'opensearch-query-workbench'; + // Shared Constants export const SQL_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/search-plugins/sql/index/'; export const PPL_DOCUMENTATION_URL = @@ -79,10 +82,13 @@ export const PPL_NEWLINE_REGEX = /[\n\r]+/g; // Observability plugin URI const BASE_OBSERVABILITY_URI = '/_plugins/_observability'; -const BASE_INTEGRATIONS_URI = '/_plugins/_integrations'; // Used later in front-end for routing +const BASE_DATACONNECTIONS_URI = '/_plugins/_query/_datasources'; export const OPENSEARCH_PANELS_API = { OBJECT: `${BASE_OBSERVABILITY_URI}/object`, }; +export const OPENSEARCH_DATACONNECTIONS_API = { + DATACONNECTION: `${BASE_DATACONNECTIONS_URI}`, +}; // Saved Objects export const SAVED_OBJECT = '/object'; @@ -90,7 +96,7 @@ export const SAVED_OBJECT = '/object'; // Color Constants export const PLOTLY_COLOR = [ '#3CA1C7', - '#8C55A3', + '#54B399', '#DB748A', '#F2BE4B', '#68CCC2', diff --git a/common/types/data_connections.ts b/common/types/data_connections.ts new file mode 100644 index 000000000..0c9228c2d --- /dev/null +++ b/common/types/data_connections.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface PermissionsConfigurationProps { + roles: Role[]; + selectedRoles: Role[]; + setSelectedRoles: React.Dispatch>; + layout: 'horizontal' | 'vertical'; +} + +export type Role = EuiComboBoxOptionOption; + +export type DatasourceType = 'S3GLUE' | 'PROMETHEUS'; diff --git a/common/types/explorer.ts b/common/types/explorer.ts index 9c5d5a943..af94da802 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -92,9 +92,7 @@ export interface IExplorerFields { } export interface EmptyTabParams { - tabIds: string[] | undefined; - queries: any | undefined; - explorerData: any | undefined; + tabIds: string[]; } export interface ILogExplorerProps { @@ -383,3 +381,28 @@ export interface VisSpecificMetaData { x_coordinate: string; y_coordinate: string; } + +export type MOMENT_UNIT_OF_TIME = + | 'years' + | 'y' + | 'quarters' + | 'Q' + | 'months' + | 'M' + | 'weeks' + | 'w' + | 'days' + | 'd' + | 'hours' + | 'h' + | 'minutes' + | 'm' + | 'seconds' + | 's' + | 'milliseconds' + | 'ms'; + +export interface GridSortingColumn { + id: string; + direction: 'asc' | 'desc'; +} diff --git a/common/utils/index.ts b/common/utils/index.ts index 524118c18..284c1f4b0 100644 --- a/common/utils/index.ts +++ b/common/utils/index.ts @@ -10,6 +10,6 @@ export { buildRawQuery, composeFinalQuery, removeBacktick, -} from './query_utils'; +} from '../../public/components/common/query_utils'; export * from './core_services'; diff --git a/common/utils/query_utils.ts b/common/utils/query_utils.ts deleted file mode 100644 index 414cf21d5..000000000 --- a/common/utils/query_utils.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import datemath from '@elastic/datemath'; -import { isEmpty } from 'lodash'; -import { - DATE_PICKER_FORMAT, - PPL_DEFAULT_PATTERN_REGEX_FILETER, -} from '../../common/constants/explorer'; -import { - PPL_INDEX_INSERT_POINT_REGEX, - PPL_INDEX_REGEX, - PPL_NEWLINE_REGEX, -} from '../../common/constants/shared'; - -/** - * @param literal - string literal that will be put inside single quotes in PPL command - * @returns string with inner single quotes escaped - */ -const escapeQuotes = (literal: string) => { - return literal.replaceAll("'", "''"); -}; - -export const getIndexPatternFromRawQuery = (query: string): string => { - const matches = query.match(PPL_INDEX_REGEX); - if (matches) { - return matches[2]; - } - return ''; -}; - -// insert time filter command and additional commands based on raw query -export const preprocessQuery = ({ - rawQuery, - startTime, - endTime, - timeField, - isLiveQuery, - selectedPatternField, - patternRegex, - filteredPattern, - whereClause, -}: { - rawQuery: string; - startTime: string; - endTime: string; - timeField?: string; - isLiveQuery: boolean; - selectedPatternField?: string; - patternRegex?: string; - filteredPattern?: string; - whereClause?: string; -}) => { - let finalQuery = ''; - - if (isEmpty(rawQuery)) return finalQuery; - - // convert to moment - const start = datemath.parse(startTime)?.utc().format(DATE_PICKER_FORMAT); - const end = datemath.parse(endTime, { roundUp: true })?.utc().format(DATE_PICKER_FORMAT); - const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); - - if (isEmpty(tokens)) return finalQuery; - - finalQuery = `${tokens![1]}=${ - tokens![2] - } | where ${timeField} >= '${start}' and ${timeField} <= '${end}'`; - - if (whereClause) { - finalQuery += ` AND ${whereClause}`; - } - - finalQuery += tokens![3]; - - if (isLiveQuery) { - finalQuery = finalQuery + ` | sort - ${timeField}`; - } - - // if a pattern is selected as filter, build it into finalQuery - if (selectedPatternField && filteredPattern) - finalQuery = buildPatternsQuery( - finalQuery, - selectedPatternField, - patternRegex, - filteredPattern - ); - - return finalQuery; -}; - -export const buildPatternsQuery = ( - baseQuery: string, - selectedPatternField?: string, - patternRegex?: string, - filteredPattern?: string -) => { - let finalQuery = baseQuery; - if (selectedPatternField) { - finalQuery += ` | patterns `; - if (patternRegex && patternRegex !== PPL_DEFAULT_PATTERN_REGEX_FILETER) { - finalQuery += `pattern='${escapeQuotes(patternRegex)}' `; - } - finalQuery += `\`${selectedPatternField}\` `; - if (filteredPattern) { - finalQuery += `| where patterns_field='${escapeQuotes(filteredPattern)}'`; - } - } - return finalQuery; -}; - -export const buildQuery = (baseQuery: string, currQuery: string) => { - let fullQuery: string; - if (baseQuery) { - fullQuery = baseQuery; - if (currQuery) { - fullQuery += '| ' + currQuery; - } - } else { - fullQuery = currQuery; - } - return fullQuery; -}; - -export const buildRawQuery = (query: any, appBaseQuery: string) => { - const rawQueryStr = (query.rawQuery as string).includes(appBaseQuery) - ? query.rawQuery - : buildQuery(appBaseQuery, query.rawQuery); - return rawQueryStr; -}; - -export const composeFinalQuery = ( - curQuery: string, - startingTime: string, - endingTime: string, - timeField: string, - isLiveQuery: boolean, - appBaseQuery: string, - selectedPatternField?: string, - patternRegex?: string, - filteredPattern?: string -) => { - const fullQuery = curQuery.includes(appBaseQuery) ? curQuery : buildQuery(appBaseQuery, curQuery); - if (isEmpty(fullQuery)) return ''; - return preprocessQuery({ - rawQuery: fullQuery, - startTime: startingTime, - endTime: endingTime, - timeField, - isLiveQuery, - selectedPatternField, - patternRegex, - filteredPattern, - }); -}; - -export const removeBacktick = (stringContainsBacktick: string) => { - if (!stringContainsBacktick) return ''; - return stringContainsBacktick.replace(/`/g, ''); -}; diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 199e51122..7ef6342c5 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -17,5 +17,6 @@ "uiActions", "urlForwarding", "visualizations" - ] -} \ No newline at end of file + ], + "optionalPlugins": ["managementOverview"] +} diff --git a/package.json b/package.json index a216027c7..40c3cccbd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "antlr4": "4.8.0", "antlr4ts": "^0.5.0-alpha.4", "mime": "^3.0.0", + "mocha": "10.1.0", "performance-now": "^2.1.0", "plotly.js-dist": "^2.2.0", "postinstall": "^0.7.4", diff --git a/public/components/app.tsx b/public/components/app.tsx index 03d9b8f4e..4569c4261 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -19,6 +19,7 @@ import { EventAnalytics } from './event_analytics'; import { Home as MetricsHome } from './metrics/index'; import { Main as NotebooksHome } from './notebooks/components/main'; import { Home as TraceAnalyticsHome } from './trace_analytics/home'; +import { Home as DataConnectionsHome } from './datasources/home'; interface ObservabilityAppDeps { CoreStartProp: CoreStart; @@ -44,6 +45,7 @@ const pages = { notebooks: NotebooksHome, dashboards: CustomPanelsHome, integrations: IntegrationsHome, + dataconnections: DataConnectionsHome, }; export const App = ({ diff --git a/public/components/application_analytics/helpers/utils.tsx b/public/components/application_analytics/helpers/utils.tsx index 5bdbcf706..08ba77f32 100644 --- a/public/components/application_analytics/helpers/utils.tsx +++ b/public/components/application_analytics/helpers/utils.tsx @@ -16,7 +16,7 @@ import { NEW_SELECTED_QUERY_TAB, TAB_CREATED_TYPE } from '../../../../common/con import { SPAN_REGEX } from '../../../../common/constants/shared'; import { VisualizationType } from '../../../../common/types/custom_panels'; import { IField } from '../../../../common/types/explorer'; -import { preprocessQuery } from '../../../../common/utils/query_utils'; +import { preprocessQuery } from '../../common/query_utils'; import { fetchVisualizationById } from '../../../components/custom_panels/helpers/utils'; import { init as initFields, diff --git a/public/components/common/query_utils/__tests__/query_utils.test.tsx b/public/components/common/query_utils/__tests__/query_utils.test.tsx new file mode 100644 index 000000000..1e185316e --- /dev/null +++ b/public/components/common/query_utils/__tests__/query_utils.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { parsePromQLIntoKeywords } from '../'; + +describe('Query Utils', () => { + describe('parsePromQLIntoKeywords', () => { + test('should parse plain catalog.metric into keywords', () => { + const query = 'catalog.metric'; + const keywords = parsePromQLIntoKeywords(query); + expect(keywords).toEqual({ + aggregation: 'avg', + attributesGroupBy: '', + connection: 'catalog', + metric: 'metric', + }); + }); + + test('should parse simple ppl query_range into keywords', () => { + console.log('should parse simple ppl query_range into keywords'); + const query = "source = test_catalog.query_range('metric')"; + const keywords = parsePromQLIntoKeywords(query); + expect(keywords).toEqual({ + aggregation: 'avg', + attributesGroupBy: '', + connection: 'test_catalog', + metric: 'metric', + }); + }); + + test('should parse promql into keywords', () => { + console.log('should parse promql into keywords'); + const query = "source = test_catalog.query_range('count by(one,two) (metric)')"; + const keywords = parsePromQLIntoKeywords(query); + expect(keywords).toEqual({ + aggregation: 'count', + attributesGroupBy: 'one,two', + connection: 'test_catalog', + metric: 'metric', + }); + }); + }); +}); diff --git a/public/components/common/query_utils/index.ts b/public/components/common/query_utils/index.ts new file mode 100644 index 000000000..98d0b54ff --- /dev/null +++ b/public/components/common/query_utils/index.ts @@ -0,0 +1,296 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dateMath from '@elastic/datemath'; +import { Moment } from 'moment-timezone'; +import { isEmpty } from 'lodash'; +import moment from 'moment'; +import { + DATE_PICKER_FORMAT, + PPL_DEFAULT_PATTERN_REGEX_FILETER, +} from '../../../../common/constants/explorer'; +import { + PPL_DATE_FORMAT, + PPL_INDEX_INSERT_POINT_REGEX, + PPL_INDEX_REGEX, + PPL_NEWLINE_REGEX, +} from '../../../../common/constants/shared'; + +/* + * "Query Utils" This file contains different reused functions in operational panels + * + * convertDateTime - Converts input datetime string to required format + */ + +/** + * @param literal - string literal that will be put inside single quotes in PPL command + * @returns string with inner single quotes escaped + */ +const escapeQuotes = (literal: string) => { + return literal.replaceAll("'", "''"); +}; + +export const convertDateTime = ( + datetime: string, + isStart = true, + formatted = true, + isMetrics: boolean = false +) => { + let returnTime: undefined | Moment; + if (isStart) { + returnTime = dateMath.parse(datetime); + } else { + returnTime = dateMath.parse(datetime, { roundUp: true }); + } + if (isMetrics) { + const myDate = new Date(returnTime._d); // Your timezone! + const epochTime = myDate.getTime() / 1000.0; + return Math.round(epochTime); + } + if (formatted) return returnTime!.utc().format(PPL_DATE_FORMAT); + return returnTime; +}; + +const PROMQL_DEFAULT_AGGREGATION = 'avg'; +const PROMQL_CATALOG_ONLY_INDEX = /^(?[^|\s]+)\.(?\S+)$/i; +const PROMQL_INDEX_REGEX = /(?[^|\s]+)\.query_range\('(?.+?)'/i; +const PROMQL_AGG_ATTR_REGEX = /(?\S+) *?(by ?\((?.*?))\)? *?\((?\S+)\)/i; +const PROMQL_METRIC_ONLY_REGEX = /^[(^\s]*?(?[_.a-z0-9]+)\)?/i; + +const parsePromQLWithMetricOnly = (promQL: string) => + promQL.match(PROMQL_METRIC_ONLY_REGEX)?.groups; +const parsePromQLWithAggAndAttrs = (promQL: string) => promQL.match(PROMQL_AGG_ATTR_REGEX)?.groups; + +interface PromQLKeywords { + connection: string; + metric: string; + aggregation: string; + attributesGroupBy: string; +} + +export const parsePromQLIntoKeywords = (query: string): PromQLKeywords | undefined => { + const catalogMatch = query.match(PROMQL_CATALOG_ONLY_INDEX); + if (catalogMatch) { + return { + ...(catalogMatch.groups as { metric: string; connection: string }), + aggregation: PROMQL_DEFAULT_AGGREGATION, + attributesGroupBy: '', + }; + } + + const indexMatch = query.match(PROMQL_INDEX_REGEX); + const { connection, promql } = indexMatch?.groups ?? {}; + if (!connection || !promql) return; + + const promTokens = parsePromQLWithAggAndAttrs(promql) || parsePromQLWithMetricOnly(promql); + if (!promTokens || !promTokens.metric) return; + + promTokens.aggregation ??= PROMQL_DEFAULT_AGGREGATION; + promTokens.attributesGroupBy ??= ''; + + return { ...promTokens, connection }; +}; + +export const getPromQLIndex = (query: string): string | undefined => { + const match = parsePromQLIntoKeywords(query); + if (!match) return; + + const { connection, metric } = match!; + if (connection && metric) return connection + '.' + metric; +}; + +export const isPromQLQuery = (query: string): boolean => !!parsePromQLIntoKeywords(query); + +export const buildPromQLFromMetricQuery = ({ + metric, + aggregation, + attributesGroupBy, +}: { + metric: string; + aggregation: string; + attributesGroupBy: string[]; +}): string => { + const attrs = attributesGroupBy.length > 0 ? `by (${attributesGroupBy.join(',')})` : ''; + + return `${aggregation} ${attrs} (${metric})`; +}; + +export const updatePromQLQueryFilters = ( + promQLQuery: string, + startTime: string, + endTime: string +) => { + const { connection, metric, aggregation, attributesGroupBy } = parsePromQLIntoKeywords( + promQLQuery + ); + console.log('updatePromQLQueryFilters', { + connection, + metric, + aggregation, + attributesGroupBy, + promQLQuery, + }); + const promQLPart = buildPromQLFromMetricQuery({ + metric, + attributesGroupBy: attributesGroupBy.split(','), + aggregation, + }); + const start = convertDateTime(startTime, true, false, true); + const end = convertDateTime(endTime, false, false, true); + return `source = ${connection}.query_range('${promQLPart}', ${start}, ${end}, '1h')`; +}; + +const getPPLIndex = (query: string): string => { + const matches = query.match(PPL_INDEX_REGEX); + if (matches) { + return matches[2]; + } + return ''; +}; + +export const getIndexPatternFromRawQuery = (query: string): string => { + return getPromQLIndex(query) || getPPLIndex(query); +}; + +// insert time filter command and additional commands based on raw query +export const preprocessQuery = ({ + rawQuery, + startTime, + endTime, + timeField, + isLiveQuery, + selectedPatternField, + patternRegex, + filteredPattern, + whereClause, +}: { + rawQuery: string; + startTime: string; + endTime: string; + timeField?: string; + isLiveQuery: boolean; + selectedPatternField?: string; + patternRegex?: string; + filteredPattern?: string; + whereClause?: string; +}) => { + let finalQuery = ''; + + if (isEmpty(rawQuery)) return finalQuery; + + // convert to moment + const start = convertDateTime(startTime, true); + const end = convertDateTime(endTime, false); + + if (!start || !end) return finalQuery; + + const formattedStart = moment(start).utc().format(DATE_PICKER_FORMAT); + const formattedEnd = moment(end).utc().format(DATE_PICKER_FORMAT); + + const promQLTokens = parsePromQLIntoKeywords(rawQuery); + + if (promQLTokens?.connection) { + return updatePromQLQueryFilters(rawQuery, startTime, endTime); + } + + const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); + + if (isEmpty(tokens)) return finalQuery; + + finalQuery = `${tokens![1]}=${ + tokens![2] + } | where ${timeField} >= '${start}' and ${timeField} <= '${end}'`; + + if (whereClause) { + finalQuery += ` AND ${whereClause}`; + } + + finalQuery += tokens![3]; + + if (isLiveQuery) { + finalQuery = finalQuery + ` | sort - ${timeField}`; + } + + // if a pattern is selected as filter, build it into finalQuery + if (selectedPatternField && filteredPattern) + finalQuery = buildPatternsQuery( + finalQuery, + selectedPatternField, + patternRegex, + filteredPattern + ); + + return finalQuery; +}; + +export const buildPatternsQuery = ( + baseQuery: string, + selectedPatternField?: string, + patternRegex?: string, + filteredPattern?: string +) => { + let finalQuery = baseQuery; + if (selectedPatternField) { + finalQuery += ` | patterns `; + if (patternRegex && patternRegex !== PPL_DEFAULT_PATTERN_REGEX_FILETER) { + finalQuery += `pattern='${escapeQuotes(patternRegex)}' `; + } + finalQuery += `\`${selectedPatternField}\` `; + if (filteredPattern) { + finalQuery += `| where patterns_field='${escapeQuotes(filteredPattern)}'`; + } + } + return finalQuery; +}; + +export const buildQuery = (baseQuery: string, currQuery: string) => { + let fullQuery: string; + if (baseQuery) { + fullQuery = baseQuery; + if (currQuery) { + fullQuery += '| ' + currQuery; + } + } else { + fullQuery = currQuery; + } + return fullQuery; +}; + +export const buildRawQuery = (query: any, appBaseQuery: string) => { + const rawQueryStr = (query.rawQuery as string).includes(appBaseQuery) + ? query.rawQuery + : buildQuery(appBaseQuery, query.rawQuery); + return rawQueryStr; +}; + +export const composeFinalQuery = ( + curQuery: string, + startingTime: string, + endingTime: string, + timeField: string, + isLiveQuery: boolean, + appBaseQuery: string, + selectedPatternField?: string, + patternRegex?: string, + filteredPattern?: string +) => { + const fullQuery = curQuery.includes(appBaseQuery) ? curQuery : buildQuery(appBaseQuery, curQuery); + if (isEmpty(fullQuery)) return ''; + return preprocessQuery({ + rawQuery: fullQuery, + startTime: startingTime, + endTime: endingTime, + timeField, + isLiveQuery, + selectedPatternField, + patternRegex, + filteredPattern, + }); +}; + +export const removeBacktick = (stringContainsBacktick: string) => { + if (!stringContainsBacktick) return ''; + return stringContainsBacktick.replace(/`/g, ''); +}; diff --git a/public/components/common/search/search.tsx b/public/components/common/search/search.tsx index 6250973ac..cf03fcee5 100644 --- a/public/components/common/search/search.tsx +++ b/public/components/common/search/search.tsx @@ -18,7 +18,6 @@ import { EuiBadge, EuiContextMenuPanel, EuiToolTip, - EuiCallOut, EuiComboBox, } from '@elastic/eui'; import { DatePicker } from './date_picker'; diff --git a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap index c6f88b0ad..c14b51650 100644 --- a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap +++ b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap @@ -1402,7 +1402,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "barmode": "group", "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -1495,7 +1495,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "barmode": "group", "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -2036,7 +2036,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -2552,7 +2552,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -3082,7 +3082,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -4726,7 +4726,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "barmode": "group", "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -4819,7 +4819,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "barmode": "group", "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", diff --git a/public/components/custom_panels/helpers/__tests__/utils.test.tsx b/public/components/custom_panels/helpers/__tests__/utils.test.tsx index 9ea806c5b..24182d643 100644 --- a/public/components/custom_panels/helpers/__tests__/utils.test.tsx +++ b/public/components/custom_panels/helpers/__tests__/utils.test.tsx @@ -20,7 +20,6 @@ import { sampleSavedVisualizationForLine, } from '../../../../../test/panels_constants'; import { - convertDateTime, displayVisualization, isDateValid, isNameValid, @@ -28,6 +27,7 @@ import { mergeLayoutAndVisualizations, onTimeChange, } from '../utils'; +import { convertDateTime } from '../../../common/query_utils'; describe('Utils helper functions', () => { configure({ adapter: new Adapter() }); diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index 2c54b670e..3ce1b9d66 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import dateMath from '@elastic/datemath'; import { ShortDate } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import _, { castArray, forEach, isEmpty } from 'lodash'; @@ -12,7 +11,6 @@ import React from 'react'; import { Layout } from 'react-grid-layout'; import { CoreStart } from '../../../../../../src/core/public'; import { - PPL_DATE_FORMAT, PPL_INDEX_REGEX, PPL_WHERE_CLAUSE_REGEX, } from '../../../../common/constants/shared'; @@ -31,12 +29,12 @@ import { ObservabilitySavedVisualization } from '../../../services/saved_objects import { getDefaultVisConfig } from '../../event_analytics/utils'; import { Visualization } from '../../visualizations/visualization'; import { MetricType } from '../../../../common/types/metrics'; +import dateMath from '@elastic/datemath'; /* * "Utils" This file contains different reused functions in Observability Dashboards * * isNameValid - Validates string to length > 0 and < 50 - * convertDateTime - Converts input datetime string to required format * mergeLayoutAndVisualizations - Function to merge current panel layout into the visualizations list * getQueryResponse - Get response of PPL query to load visualizations * renderSavedVisualization - Fetches savedVisualization by Id and runs getQueryResponse diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx index 7a629952e..7b49f5aa4 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx @@ -49,13 +49,13 @@ import { SavedObjectsActions } from '../../../../services/saved_objects/saved_ob import { ObservabilitySavedVisualization } from '../../../../services/saved_objects/saved_object_client/types'; import { FlyoutContainers } from '../../../common/flyout_containers'; import { - convertDateTime, displayVisualization, getQueryResponse, isDateValid, parseSavedVisualizations, } from '../../helpers/utils'; import './visualization_flyout.scss'; +import { convertDateTime } from '../../../common/query_utils'; /* * VisaulizationFlyout - This module create a flyout to add visualization diff --git a/public/components/datasources/components/__tests__/__snapshots__/connection_details.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/connection_details.test.tsx.snap new file mode 100644 index 000000000..104567f9d --- /dev/null +++ b/public/components/datasources/components/__tests__/__snapshots__/connection_details.test.tsx.snap @@ -0,0 +1,550 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Connection Details test Renders connection details for prometheus datasource 1`] = ` + + +
+ + + +
+
+ + + Configurations may be managed elsewhere. + +
+ +
+ +
+ Access to data can be managed in other systems outside of OpenSearch. Check with your administrator for additional configurations. +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ +
+

+ Data source configurations +

+ Control configurations for your data source. +
+
+
+
+
+
+
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ Data source name +
+
+ +
+ prom +
+
+
+
+ +
+ +
+ Data source description +
+
+ +
+ - +
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+ Prometheus URI +
+
+ +
+ localhost:9201 +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ + +`; + +exports[`Connection Details test Renders connection details for s3 datasource 1`] = ` + + +
+ + + +
+
+ + + Configurations may be managed elsewhere. + +
+ +
+ +
+ Access to data can be managed in other systems outside of OpenSearch. Check with your administrator for additional configurations. +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ +
+

+ Data source configurations +

+ Control configurations for your data source. +
+
+
+
+
+
+
+ +
+
+ + +
+ +
+ +
+ +
+ +
+ Data source name +
+
+ +
+ ya +
+
+
+
+ +
+ +
+ Data source description +
+
+ +
+ - +
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+ Index store region +
+
+ +
+ us-west-2 +
+
+
+
+ +
+ +
+ Index store URI +
+
+ +
+ y +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ + +`; diff --git a/public/components/datasources/components/__tests__/__snapshots__/data_connection.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/data_connection.test.tsx.snap new file mode 100644 index 000000000..51ae56882 --- /dev/null +++ b/public/components/datasources/components/__tests__/__snapshots__/data_connection.test.tsx.snap @@ -0,0 +1,856 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data Connection Page test Renders Prometheus data connection page with data 1`] = ` +
+
+
+
+
+
+
+

+

+
+
+
+
+
+
+
+
+
+ Connection title +
+
+ prom +
+
+
+
+ Data source description +
+
+ - +
+
+
+
+
+
+
+
+ Prometheus URI +
+
+ localhost:9201 +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Query your data in Metrics Analytics. +

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + Configurations may be managed elsewhere. + +
+
+
+ Access to data can be managed in other systems outside of OpenSearch. Check with your administrator for additional configurations. +
+
+
+
+
+
+
+
+

+ Access Control +

+ Control which OpenSearch users have access to this data source. +
+
+
+
+
+
+
+
+
+ Query access +
+
+ Everyone +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Data Connection Page test Renders S3 data connection page with data 1`] = ` +
+
+
+
+
+
+
+

+

+
+
+
+
+
+
+
+
+
+ Connection title +
+
+ ya +
+
+
+
+ Data source description +
+
+ - +
+
+
+
+
+
+
+
+ Index store region +
+
+ us-west-2 +
+
+
+
+ Index store URI +
+
+ y +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Query your data in Data Explorer or Observability Logs. +

+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Accelerate performance through OpenSearch indexing. +

+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Manually Define Tables +

+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Explore data faster through integrations +

+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + Configurations may be managed elsewhere. + +
+
+
+ Access to data can be managed in other systems outside of OpenSearch. Check with your administrator for additional configurations. +
+
+
+
+
+
+
+
+

+ Access Control +

+ Control which OpenSearch users have access to this data source. +
+
+
+
+
+
+
+
+
+ Query access +
+
+ Everyone +
+
+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_description.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_description.test.tsx.snap new file mode 100644 index 000000000..55b247e8a --- /dev/null +++ b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_description.test.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Manage Data Connections Description test Renders manage data connections description 1`] = ` + +
+ +
+ +
+ +

+ Manage existing data sources +

+
+ +
+ + +
+ +
+ Manage already created data sources or + + + + create a new data source + + +
+
+
+
+
+ + +
+ + + + + +
+
+
+ + +
+
+
+ +`; diff --git a/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap new file mode 100644 index 000000000..f3d51f2ce --- /dev/null +++ b/public/components/datasources/components/__tests__/__snapshots__/manage_data_connections_table.test.tsx.snap @@ -0,0 +1,900 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Manage Data Connections Table test Renders manage data connections table with data 1`] = ` +
+
+
+
+
+
+

+ Data sources +

+
+
+
+
+
+ Connect and manage compatible OpenSearch and OpenSearch Dashboard data sources. + + + Learn more + + +
+
+
+
+ + +
+
+
+
+
+
+
+

+ Manage existing data sources +

+
+
+
+ Manage already created data sources or + + + create a new data source + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Name + + + + + + Actions + + +
+
+ Name +
+
+
+ +
+
+
+ + + Edit + + + + Query in Observability Logs + +
+
+
+ + + +
+
+
+
+
+
+ Name +
+
+
+ +
+
+
+ + + Edit + + + + Query in Observability Logs + +
+
+
+ + + +
+
+
+
+
+
+ Name +
+
+
+ +
+
+
+ + + Edit + + + + Query in Observability Logs + +
+
+
+ + + +
+
+
+
+
+
+ Name +
+
+
+ +
+
+
+ + + Edit + + + + Query in Observability Logs + +
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/public/components/datasources/components/__tests__/__snapshots__/no_access.test.tsx.snap b/public/components/datasources/components/__tests__/__snapshots__/no_access.test.tsx.snap new file mode 100644 index 000000000..89c389ab8 --- /dev/null +++ b/public/components/datasources/components/__tests__/__snapshots__/no_access.test.tsx.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`No access test Renders no access view of data source 1`] = ` + + +
+ + Return to data connections + + } + body={ + + You are missing permissions to view connection details. Contact your administrator for permissions. + + } + iconType="alert" + title={ +

+ No permissions to access +

+ } + > +
+ + + + + + +
+ + +

+ No permissions to access +

+
+ + + +
+ + +
+ +
+ You are missing permissions to view connection details. Contact your administrator for permissions. +
+
+
+
+ + + +
+ + + + + + +
+ +
+ + +`; diff --git a/public/components/datasources/components/__tests__/connection_details.test.tsx b/public/components/datasources/components/__tests__/connection_details.test.tsx new file mode 100644 index 000000000..c5a47a1f4 --- /dev/null +++ b/public/components/datasources/components/__tests__/connection_details.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { + testPrometheusConnectionDetails, + testS3ConnectionDetails, +} from '../../../../../test/datasources'; +import { ConnectionDetails } from '../manage/connection_details'; + +describe('Connection Details test', () => { + configure({ adapter: new Adapter() }); + + it('Renders connection details for s3 datasource', async () => { + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('Renders connection details for prometheus datasource', async () => { + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/datasources/components/__tests__/data_connection.test.tsx b/public/components/datasources/components/__tests__/data_connection.test.tsx new file mode 100644 index 000000000..a7c817626 --- /dev/null +++ b/public/components/datasources/components/__tests__/data_connection.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { act } from '@testing-library/react'; +import React from 'react'; +import { + describePrometheusDataConnection, + describeS3Dataconnection, +} from '../../../../../test/datasources'; +import { DataConnection } from '../manage/data_connection'; +import ReactDOM from 'react-dom'; +import { coreRefs } from '../../../../../public/framework/core_refs'; + +jest.mock('../../../../../public/framework/core_refs', () => ({ + coreRefs: { + chrome: { + setBreadcrumbs: jest.fn(), + }, + http: { + get: jest.fn(), + }, + }, +})); + +describe('Data Connection Page test', () => { + configure({ adapter: new Adapter() }); + + beforeEach(() => { + // Clear the mock implementation before each test + (coreRefs.http!.get as jest.Mock).mockClear(); + }); + + it('Renders Prometheus data connection page with data', async () => { + const pplService = { + fetch: jest.fn(), + }; + const container = document.createElement('div'); + (coreRefs.http!.get as jest.Mock).mockResolvedValue(describePrometheusDataConnection); + await act(() => { + ReactDOM.render(, container); + }); + expect(container).toMatchSnapshot(); + }); + + it('Renders S3 data connection page with data', async () => { + const pplService = { + fetch: jest.fn(), + }; + const container = document.createElement('div'); + (coreRefs.http!.get as jest.Mock).mockResolvedValue(describeS3Dataconnection); + await act(() => { + ReactDOM.render(, container); + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/components/datasources/components/__tests__/manage_data_connections_description.test.tsx b/public/components/datasources/components/__tests__/manage_data_connections_description.test.tsx new file mode 100644 index 000000000..3f9500075 --- /dev/null +++ b/public/components/datasources/components/__tests__/manage_data_connections_description.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { DataConnectionsDescription } from '../manage/manage_data_connections_description'; + +describe('Manage Data Connections Description test', () => { + configure({ adapter: new Adapter() }); + + it('Renders manage data connections description', async () => { + const wrapper = mount( {}} />); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/datasources/components/__tests__/manage_data_connections_table.test.tsx b/public/components/datasources/components/__tests__/manage_data_connections_table.test.tsx new file mode 100644 index 000000000..9c4979cff --- /dev/null +++ b/public/components/datasources/components/__tests__/manage_data_connections_table.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { act } from '@testing-library/react'; +import React from 'react'; +import { ManageDataConnectionsTable } from '../manage/manage_data_connections_table'; +import { showDataConnectionsData } from '../../../../../test/datasources'; +import ReactDOM from 'react-dom'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: '/manage', + }), +})); + +describe('Manage Data Connections Table test', () => { + configure({ adapter: new Adapter() }); + + it('Renders manage data connections table with data', async () => { + const http = { + get: jest.fn().mockResolvedValue(showDataConnectionsData), + }; + const pplService = { + fetch: jest.fn().mockResolvedValue(showDataConnectionsData), + }; + const mockChrome = { + setBreadcrumbs: jest.fn(), + }; + const container = document.createElement('div'); + await act(() => { + ReactDOM.render( + , + container + ); + }); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/public/components/datasources/components/__tests__/no_access.test.tsx b/public/components/datasources/components/__tests__/no_access.test.tsx new file mode 100644 index 000000000..191872613 --- /dev/null +++ b/public/components/datasources/components/__tests__/no_access.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { NoAccess } from '../no_access'; + +describe('No access test', () => { + configure({ adapter: new Adapter() }); + + it('Renders no access view of data source', async () => { + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/datasources/components/data_connections_header.tsx b/public/components/datasources/components/data_connections_header.tsx new file mode 100644 index 000000000..42739c0aa --- /dev/null +++ b/public/components/datasources/components/data_connections_header.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiLink, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTab, + EuiTabs, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { OPENSEARCH_DOCUMENTATION_URL } from '../../../../common/constants/data_connections'; + +const tabs = [ + { + id: 'manage', + name: 'Manage data source', + disabled: false, + }, + { + id: 'new', + name: 'New data source', + disabled: false, + }, +]; + +export const DataConnectionsHeader = () => { + const location = useLocation().pathname.substring(1); + + const [selectedTabId, setSelectedTabId] = useState(location ? location : 'manage'); + + const onSelectedTabChanged = (id) => { + setSelectedTabId(id); + window.location.hash = id; + }; + + const renderTabs = () => { + return tabs.map((tab, index) => ( + onSelectedTabChanged(tab.id)} + isSelected={tab.id === selectedTabId} + disabled={tab.disabled} + key={index} + data-test-subj={tab.id} + > + {tab.name} + + )); + }; + + return ( +
+ + + +

Data sources

+
+
+
+ + + Connect and manage compatible OpenSearch and OpenSearch Dashboard data sources.{' '} + + Learn more + + + + {renderTabs()} + +
+ ); +}; diff --git a/public/components/datasources/components/manage/access_control_tab.tsx b/public/components/datasources/components/manage/access_control_tab.tsx new file mode 100644 index 000000000..bb56bccaf --- /dev/null +++ b/public/components/datasources/components/manage/access_control_tab.tsx @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { ConnectionManagementCallout } from './connection_management_callout'; +import { Role } from '../../../../../common/types/data_connections'; + +interface AccessControlTabProps { + dataConnection: string; + connector: string; + properties: unknown; + allowedRoles: string[]; +} + +export const AccessControlTab = (props: AccessControlTabProps) => { + const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState( + props.allowedRoles.map((role) => { + return { label: role }; + }) + ); + + const AccessControlDetails = () => { + return ( + + + + + Query access + + {selectedQueryPermissionRoles.length + ? `Restricted to ${selectedQueryPermissionRoles + .map((role) => role.label) + .join(',')}` + : 'Everyone'} + + + + + + ); + }; + + const AccessControlHeader = () => { + return ( + + + +

Access Control

+ Control which OpenSearch users have access to this data source. +
+
+
+ ); + }; + + return ( + <> + + + + + + + + + + + + ); +}; diff --git a/public/components/datasources/components/manage/connection_details.tsx b/public/components/datasources/components/manage/connection_details.tsx new file mode 100644 index 000000000..f78272b73 --- /dev/null +++ b/public/components/datasources/components/manage/connection_details.tsx @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiHorizontalRule } from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; +import { ConnectionManagementCallout } from './connection_management_callout'; +import { PrometheusProperties, S3GlueProperties } from './data_connection'; +import { DatasourceType } from '../../../../../common/types/data_connections'; + +interface ConnectionDetailProps { + dataConnection: string; + connector: DatasourceType; + description: string; + properties: S3GlueProperties | PrometheusProperties; +} + +export const ConnectionDetails = (props: ConnectionDetailProps) => { + const { dataConnection, connector, description, properties } = props; + + const S3ConnectionConfigurationView = () => { + return ( + + + + + Data source name + + {dataConnection} + + + + Data source description + + {description || '-'} + + + + + + + + Index store region + + {(properties as S3GlueProperties)['glue.indexstore.opensearch.region'] || '-'} + + + + Index store URI + + {(properties as S3GlueProperties)['glue.indexstore.opensearch.uri'] || '-'} + + + + + + ); + }; + + const PrometheusConnectionConfigurationView = () => { + return ( + + + + + Data source name + + {dataConnection} + + + + Data source description + + {description || '-'} + + + + + + + + Prometheus URI + + {(properties as PrometheusProperties)['prometheus.uri'] || '-'} + + + + + + ); + }; + + const ConnectionConfigurationHeader = () => { + return ( + + + +

Data source configurations

+ Control configurations for your data source. +
+
+
+ ); + }; + + return ( + <> + + + + + + + {connector === 'S3GLUE' ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/public/components/datasources/components/manage/connection_management_callout.tsx b/public/components/datasources/components/manage/connection_management_callout.tsx new file mode 100644 index 000000000..b78872287 --- /dev/null +++ b/public/components/datasources/components/manage/connection_management_callout.tsx @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; + +export const ConnectionManagementCallout = () => { + return ( + + Access to data can be managed in other systems outside of OpenSearch. Check with your + administrator for additional configurations. + + ); +}; diff --git a/public/components/datasources/components/manage/data_connection.tsx b/public/components/datasources/components/manage/data_connection.tsx new file mode 100644 index 000000000..2f18165ed --- /dev/null +++ b/public/components/datasources/components/manage/data_connection.tsx @@ -0,0 +1,317 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiAccordion, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiSpacer, + EuiTabbedContent, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { + DATACONNECTIONS_BASE, + observabilityIntegrationsID, + observabilityLogsID, + observabilityMetricsID, + queryWorkbenchPluginID, +} from '../../../../../common/constants/shared'; +import { coreRefs } from '../../../../framework/core_refs'; +import { NoAccess } from '../no_access'; +import { AccessControlTab } from './access_control_tab'; +import { ConnectionDetails } from './connection_details'; +import { DatasourceType } from '../../../../../common/types/data_connections'; + +interface DatasourceDetails { + allowedRoles: string[]; + name: string; + connector: DatasourceType; + description: string; + properties: S3GlueProperties | PrometheusProperties; +} + +export interface S3GlueProperties { + 'glue.indexstore.opensearch.uri': string; + 'glue.indexstore.opensearch.region': string; +} + +export interface PrometheusProperties { + 'prometheus.uri': string; +} + +export const DataConnection = (props: any) => { + const { dataSource } = props; + const [datasourceDetails, setDatasourceDetails] = useState({ + allowedRoles: [], + name: '', + description: '', + connector: 'PROMETHEUS', + properties: { 'prometheus.uri': 'placeholder' }, + }); + const [hasAccess, setHasAccess] = useState(true); + const { http, chrome, application } = coreRefs; + + const DefaultDatasourceCards = () => { + return ( + + + } + title={'Query data'} + description="Query your data in Data Explorer or Observability Logs." + onClick={() => application!.navigateToApp(observabilityLogsID)} + /> + + + } + title={'Accelerate performance'} + description="Accelerate performance through OpenSearch indexing." + onClick={() => + application!.navigateToApp(queryWorkbenchPluginID, { + path: `#/accelerate/${dataSource}`, + }) + } + /> + + + } + title={'Tables'} + description="Manually Define Tables" + onClick={() => + application!.navigateToApp(queryWorkbenchPluginID, { + path: `#/${dataSource}`, + }) + } + /> + + + } + title={'Integrations data'} + description="Explore data faster through integrations" + onClick={() => application!.navigateToApp(observabilityIntegrationsID)} + /> + + + ); + }; + + useEffect(() => { + chrome!.setBreadcrumbs([ + { + text: 'Data sources', + href: '#/', + }, + { + text: `${dataSource}`, + href: `#/manage/${dataSource}`, + }, + ]); + http! + .get(`${DATACONNECTIONS_BASE}/${dataSource}`) + .then((data) => { + setDatasourceDetails({ + allowedRoles: data.allowedRoles, + description: data.description, + name: data.name, + connector: data.connector, + properties: data.properties, + }); + }) + .catch((err) => { + setHasAccess(false); + }); + }, [chrome, http]); + + const tabs = [ + { + id: 'access_control', + name: 'Access control', + disabled: false, + content: ( + + ), + }, + { + id: 'connection_configuration', + name: 'Connection configuration', + disabled: false, + content: ( + + ), + }, + ]; + + const QueryOrAccelerateData = () => { + switch (datasourceDetails.connector) { + case 'S3GLUE': + return ; + case 'PROMETHEUS': + // Prometheus does not have acceleration or integrations, and should go to metrics analytics + return ( + + + } + title={'Query data'} + description="Query your data in Metrics Analytics." + onClick={() => application!.navigateToApp(observabilityMetricsID)} + /> + + + ); + default: + return ; + } + }; + + const S3DatasourceOverview = () => { + return ( + + + + + + Connection title + + {datasourceDetails.name || '-'} + + + + Data source description + + {datasourceDetails.description || '-'} + + + + + + + + Index store region + + {(datasourceDetails.properties as S3GlueProperties)[ + 'glue.indexstore.opensearch.region' + ] || '-'} + + + + Index store URI + + {(datasourceDetails.properties as S3GlueProperties)[ + 'glue.indexstore.opensearch.uri' + ] || '-'} + + + + + + + + ); + }; + + const PrometheusDatasourceOverview = () => { + return ( + + + + + + Connection title + + {datasourceDetails.name || '-'} + + + + Data source description + + {datasourceDetails.description || '-'} + + + + + + + + Prometheus URI + + {(datasourceDetails.properties as PrometheusProperties)['prometheus.uri'] || '-'} + + + + + + + + ); + }; + + const DatasourceOverview = () => { + switch (datasourceDetails.connector) { + case 'S3GLUE': + return ; + case 'PROMETHEUS': + return ; + } + }; + + if (!hasAccess) { + return ; + } + + return ( + + + + + + + +

{dataSource}

+
+
+
+
+
+ + + + + + + + + +
+
+ ); +}; diff --git a/public/components/datasources/components/manage/manage_data_connections_description.tsx b/public/components/datasources/components/manage/manage_data_connections_description.tsx new file mode 100644 index 000000000..3146a8b99 --- /dev/null +++ b/public/components/datasources/components/manage/manage_data_connections_description.tsx @@ -0,0 +1,50 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiSpacer, + EuiText, + EuiTitle, + EuiHorizontalRule, + EuiLink, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import React from 'react'; + +interface DataConnectionsDescriptionProps { + refresh: () => void; +} + +export const DataConnectionsDescription = (props: DataConnectionsDescriptionProps) => { + const { refresh } = props; + return ( +
+ + + <> + +

Manage existing data sources

+
+ + + + Manage already created data sources or{' '} + create a new data source + + +
+ + + Refresh + + +
+ + +
+ ); +}; diff --git a/public/components/datasources/components/manage/manage_data_connections_table.tsx b/public/components/datasources/components/manage/manage_data_connections_table.tsx new file mode 100644 index 000000000..d2fe01326 --- /dev/null +++ b/public/components/datasources/components/manage/manage_data_connections_table.tsx @@ -0,0 +1,227 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + EuiLink, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { DataConnectionsHeader } from '../data_connections_header'; +import { HomeProps } from '../../home'; +import { DataConnectionsDescription } from './manage_data_connections_description'; +import { + DATACONNECTIONS_BASE, + observabilityLogsID, + observabilityMetricsID, +} from '../../../../../common/constants/shared'; +import { useToast } from '../../../common/toast'; +import { DeleteModal } from '../../../common/helpers/delete_modal'; +import S3Logo from '../../icons/s3-logo.svg'; +import PrometheusLogo from '../../icons/prometheus-logo.svg'; +import { DatasourceType } from '../../../../../common/types/data_connections'; +import { coreRefs } from '../../../../../public/framework/core_refs'; + +interface DataConnection { + connectionType: DatasourceType; + name: string; +} + +export const ManageDataConnectionsTable = (props: HomeProps) => { + const { http, chrome, pplService } = props; + const { application } = coreRefs; + + const { setToast } = useToast(); + + const [data, setData] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); + + const deleteConnection = (connectionName: string) => { + http! + .delete(`${DATACONNECTIONS_BASE}/${connectionName}`) + .then(() => { + setToast(`Data connection ${connectionName} deleted successfully`); + setData( + data.filter((connection) => { + return !(connection.name === connectionName); + }) + ); + }) + .catch((err) => { + setToast(`Data connection $${connectionName} not deleted. See output for more details.`); + }); + }; + + useEffect(() => { + chrome.setBreadcrumbs([ + { + text: 'Data sources', + href: '#/', + }, + ]); + handleDataRequest(); + }, [chrome]); + + async function handleDataRequest() { + pplService!.fetch({ query: 'show datasources', format: 'jdbc' }).then((dataconnections) => + setData( + dataconnections.jsonData.map((x: any) => { + return { name: x.DATASOURCE_NAME, connectionType: x.CONNECTOR_TYPE }; + }) + ) + ); + } + + const displayDeleteModal = (connectionName: string) => { + setModalLayout( + { + setIsModalVisible(false); + deleteConnection(connectionName); + }} + onCancel={() => { + setIsModalVisible(false); + }} + title={`Delete ${connectionName}`} + message={`Are you sure you want to delete ${connectionName}?`} + /> + ); + setIsModalVisible(true); + }; + + const actions = [ + { + name: 'Edit', + isPrimary: true, + icon: 'pencil', + type: 'icon', + onClick: (datasource: DataConnection) => { + window.location.href = `#/manage/${datasource.name}`; + }, + 'data-test-subj': 'action-edit', + }, + { + name: (datasource: DataConnection) => + `Query in ${ + datasource.connectionType === 'PROMETHEUS' ? 'Metrics Analytics' : 'Observability Logs' + }`, + isPrimary: true, + icon: 'discoverApp', + type: 'icon', + onClick: (datasource: DataConnection) => { + application!.navigateToApp( + datasource.connectionType === 'PROMETHEUS' ? observabilityMetricsID : observabilityLogsID + ); + }, + 'data-test-subj': 'action-query', + }, + { + name: 'Accelerate performance', + isPrimary: false, + icon: 'bolt', + type: 'icon', + available: (datasource: DataConnection) => datasource.connectionType !== 'PROMETHEUS', + onClick: () => { + application!.navigateToApp('opensearch-query-workbench'); + }, + 'data-test-subj': 'action-accelerate', + }, + { + name: 'Delete', + description: 'Delete this data source', + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: (datasource: DataConnection) => displayDeleteModal(datasource.name), + isPrimary: false, + 'data-test-subj': 'action-delete', + }, + ]; + + const icon = (record: DataConnection) => { + switch (record.connectionType) { + case 'S3GLUE': + return ; + case 'PROMETHEUS': + return ; + default: + return <>; + } + }; + + const tableColumns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + render: (value, record: DataConnection) => ( + + {icon(record)} + + + {_.truncate(record.name, { length: 100 })} + + + + ), + }, + { + field: 'actions', + name: 'Actions', + actions, + }, + ] as Array>; + + const search = { + box: { + incremental: true, + }, + }; + + const entries = data.map((dataconnection: DataConnection) => { + const name = dataconnection.name; + const connectionType = dataconnection.connectionType; + return { connectionType, name, data: { name, connectionType } }; + }); + + return ( + + + + + + + + + {isModalVisible && modalLayout} + + + ); +}; diff --git a/public/components/datasources/components/new/auth_details.tsx b/public/components/datasources/components/new/auth_details.tsx new file mode 100644 index 000000000..313588df4 --- /dev/null +++ b/public/components/datasources/components/new/auth_details.tsx @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFormRow, EuiFieldText, EuiFieldPassword } from '@elastic/eui'; +import { useState } from 'react'; +import React from 'react'; +import { AuthMethod } from '../../../../../common/constants/data_connections'; + +interface AuthDetailProps { + currentAuthMethod: AuthMethod; + currentPassword: string; + currentUsername: string; + currentAccessKey?: string; + currentSecretKey?: string; + currentRegion?: string; + setRegionForRequest?: React.Dispatch>; + setAccessKeyForRequest?: React.Dispatch>; + setSecretKeyForRequest?: React.Dispatch>; + setPasswordForRequest: React.Dispatch>; + setUsernameForRequest: React.Dispatch>; +} + +export const AuthDetails = (props: AuthDetailProps) => { + const { + currentUsername, + currentPassword, + currentAccessKey, + currentSecretKey, + currentRegion, + currentAuthMethod, + setAccessKeyForRequest, + setPasswordForRequest, + setRegionForRequest, + setSecretKeyForRequest, + setUsernameForRequest, + } = props; + const [password, setPassword] = useState(currentPassword); + const [username, setUsername] = useState(currentUsername); + const [accessKey, setAccessKey] = useState(currentAccessKey); + const [secretKey, setSecretKey] = useState(currentSecretKey); + const [region, setRegion] = useState(currentRegion); + switch (currentAuthMethod) { + case 'basicauth': + return ( + <> + + setUsername(e.target.value)} + onBlur={(e) => setUsernameForRequest(e.target.value)} + /> + + + setPassword(e.target.value)} + onBlur={(e) => setPasswordForRequest(e.target.value)} + /> + + + ); + case 'awssigv4': + return ( + <> + + { + setRegionForRequest(e.target.value); + }} + onChange={(e) => { + setRegion(e.target.value); + }} + /> + + + setAccessKey(e.target.value)} + onBlur={(e) => setAccessKeyForRequest(e.target.value)} + /> + + + setSecretKey(e.target.value)} + onBlur={(e) => setSecretKeyForRequest(e.target.value)} + /> + + + ); + default: + return null; + } +}; diff --git a/public/components/datasources/components/new/configure_datasource.tsx b/public/components/datasources/components/new/configure_datasource.tsx new file mode 100644 index 000000000..41b506757 --- /dev/null +++ b/public/components/datasources/components/new/configure_datasource.tsx @@ -0,0 +1,310 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiButton, + EuiSteps, + EuiPageSideBar, + EuiBottomBar, + EuiButtonEmpty, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; +import { ConfigureS3Datasource } from './configure_s3_datasource'; +import { coreRefs } from '../../../../../public/framework/core_refs'; +import { DATACONNECTIONS_BASE } from '../../../../../common/constants/shared'; +import { ReviewS3Datasource } from './review_s3_datasource_configuration'; +import { useToast } from '../../../../../public/components/common/toast'; +import { DatasourceType, Role } from '../../../../../common/types/data_connections'; +import { ConfigurePrometheusDatasource } from './configure_prometheus_datasource'; +import { ReviewPrometheusDatasource } from './review_prometheus_datasource_configuration'; +import { + AuthMethod, + DatasourceTypeToDisplayName, +} from '../../../../../common/constants/data_connections'; +import { formatError } from '../../../../../public/components/event_analytics/utils'; +import { NotificationsStart } from '../../../../../../../src/core/public'; + +interface ConfigureDatasourceProps { + type: DatasourceType; + notifications: NotificationsStart; +} + +export function Configure(props: ConfigureDatasourceProps) { + const { type, notifications } = props; + const { http, chrome } = coreRefs; + const { setToast } = useToast(); + const [authMethod, setAuthMethod] = useState('basicauth'); + const [name, setName] = useState(''); + const [details, setDetails] = useState(''); + const [arn, setArn] = useState(''); + const [storeURI, setStoreURI] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [accessKey, setAccessKey] = useState(''); + const [secretKey, setSecretKey] = useState(''); + const [region, setRegion] = useState(''); + const [roles, setRoles] = useState([]); + const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState([]); + const [page, setPage] = useState<'configure' | 'review'>('configure'); + const ConfigureDatasourceSteps = [ + { + title: 'Configure Data Source', + status: page === 'review' ? 'complete' : undefined, + }, + { + title: 'Review Configuration', + }, + ]; + + useEffect(() => { + http!.get('/api/v1/configuration/roles').then((data) => + setRoles( + Object.keys(data.data).map((key) => { + return { label: key }; + }) + ) + ); + chrome!.setBreadcrumbs([ + { + text: 'Data sources', + href: '#/', + }, + { + text: 'New', + href: '#/new', + }, + { + text: `${DatasourceTypeToDisplayName[type]}`, + href: `#/configure/${type}`, + }, + ]); + }, []); + + const ConfigureDatasource = (configurationProps: { datasourceType: DatasourceType }) => { + const { datasourceType } = configurationProps; + switch (datasourceType) { + case 'S3GLUE': + return ( + + ); + case 'PROMETHEUS': + return ( + + ); + default: + return <>; + } + }; + + const ReviewDatasourceConfiguration = (configurationProps: { datasourceType: string }) => { + const { datasourceType } = configurationProps; + switch (datasourceType) { + case 'S3GLUE': + return ( + setPage('configure')} + /> + ); + case 'PROMETHEUS': + return ( + setPage('configure')} + /> + ); + default: + return <>; + } + }; + + const ReviewSaveOrCancel = useCallback(() => { + return ( + + + + { + window.location.hash = '#/new'; + }} + color="ghost" + size="s" + iconType="cross" + > + Cancel + + + + (page === 'review' ? setPage('configure') : {})} + color="ghost" + size="s" + iconType="arrowLeft" + > + Previous + + + + (page === 'review' ? createDatasource() : setPage('review'))} + size="s" + iconType="arrowRight" + fill + > + {page === 'configure' + ? `Review Configuration` + : `Connect to ${DatasourceTypeToDisplayName[type]}`} + + + + + ); + }, [page]); + + const createDatasource = () => { + let response; + switch (type) { + case 'S3GLUE': + const s3properties = + authMethod === 'basicauth' + ? { + 'glue.auth.type': 'iam_role', + 'glue.auth.role_arn': arn, + 'glue.indexstore.opensearch.uri': storeURI, + 'glue.indexstore.opensearch.auth': authMethod, + 'glue.indexstore.opensearch.auth.username': username, + 'glue.indexstore.opensearch.auth.password': password, + } + : { + 'glue.auth.type': 'iam_role', + 'glue.auth.role_arn': arn, + 'glue.indexstore.opensearch.uri': storeURI, + 'glue.indexstore.opensearch.auth': authMethod, + }; + response = http!.post(`${DATACONNECTIONS_BASE}`, { + body: JSON.stringify({ + name, + allowedRoles: selectedQueryPermissionRoles.map((role) => role.label), + connector: 's3glue', + properties: s3properties, + }), + }); + break; + case 'PROMETHEUS': + const prometheusProperties = + authMethod === 'basicauth' + ? { + 'prometheus.uri': storeURI, + 'prometheus.auth.type': authMethod, + 'prometheus.auth.username': username, + 'prometheus.auth.password': password, + } + : { + 'prometheus.uri': storeURI, + 'prometheus.auth.type': authMethod, + 'prometheus.auth.region': region, + 'prometheus.auth.access_key': accessKey, + 'prometheus.auth.secret_key': secretKey, + }; + response = http!.post(`${DATACONNECTIONS_BASE}`, { + body: JSON.stringify({ + name, + allowedRoles: selectedQueryPermissionRoles.map((role) => role.label), + connector: 'prometheus', + properties: prometheusProperties, + }), + }); + break; + default: + response = Promise.reject('Invalid data source type'); + } + response + .then(() => { + setToast(`Data source ${name} created`, 'success'); + window.location.hash = '#/manage'; + }) + .catch((err) => { + const formattedError = formatError(err.name, err.message, err.body.message); + notifications.toasts.addError(formattedError, { + title: 'Could not create data source', + }); + setPage('configure'); + }); + }; + + return ( + + + + + + {page === 'configure' ? ( + + ) : ( + + )} + + + + + + ); +} diff --git a/public/components/datasources/components/new/configure_prometheus_datasource.tsx b/public/components/datasources/components/new/configure_prometheus_datasource.tsx new file mode 100644 index 000000000..7c74e0651 --- /dev/null +++ b/public/components/datasources/components/new/configure_prometheus_datasource.tsx @@ -0,0 +1,208 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiLink, + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiSelect, + EuiFieldPassword, + EuiForm, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { + AuthMethod, + OPENSEARCH_DOCUMENTATION_URL, +} from '../../../../../common/constants/data_connections'; +import { QueryPermissionsConfiguration } from './query_permissions'; +import { Role } from '../../../../../common/types/data_connections'; +import { AuthDetails } from './auth_details'; + +interface ConfigurePrometheusDatasourceProps { + roles: Role[]; + selectedQueryPermissionRoles: Role[]; + setSelectedQueryPermissionRoles: React.Dispatch>; + currentName: string; + currentDetails: string; + currentStore: string; + currentUsername: string; + currentPassword: string; + currentAccessKey: string; + currentSecretKey: string; + currentRegion: string; + currentAuthMethod: AuthMethod; + setAuthMethodForRequest: React.Dispatch>; + setRegionForRequest: React.Dispatch>; + setAccessKeyForRequest: React.Dispatch>; + setSecretKeyForRequest: React.Dispatch>; + setPasswordForRequest: React.Dispatch>; + setUsernameForRequest: React.Dispatch>; + setStoreForRequest: React.Dispatch>; + setNameForRequest: React.Dispatch>; + setDetailsForRequest: React.Dispatch>; +} + +export const ConfigurePrometheusDatasource = (props: ConfigurePrometheusDatasourceProps) => { + const { + setNameForRequest, + setDetailsForRequest, + setStoreForRequest, + currentStore, + currentName, + currentDetails, + roles, + selectedQueryPermissionRoles, + setSelectedQueryPermissionRoles, + currentUsername, + setUsernameForRequest, + currentPassword, + setPasswordForRequest, + currentAccessKey, + setAccessKeyForRequest, + currentSecretKey, + setSecretKeyForRequest, + currentRegion, + setRegionForRequest, + currentAuthMethod, + setAuthMethodForRequest, + } = props; + + const [name, setName] = useState(currentName); + const [details, setDetails] = useState(currentDetails); + const [store, setStore] = useState(currentStore); + const authOptions = [ + { value: 'basicauth', text: 'Basic authentication' }, + { value: 'awssigv4', text: 'AWS Signature Version 4' }, + ]; + + return ( +
+ + +

{`Configure Prometheus Data Source`}

+
+ + + {`Connect to Prometheus with OpenSearch and OpenSearch Dashboards `} + + Learn more + + + + + +

Data source details

+
+ + + <> + +

+ This is the name the connection will be referenced by in OpenSearch Dashboards. It + is recommended to make this short yet descriptive to help users when selecting a + connection. +

+
+ { + setName(e.target.value); + }} + onBlur={(e) => { + setNameForRequest(e.target.value); + }} + /> + +
+ + { + setDetailsForRequest(e.target.value); + }} + onChange={(e) => { + setDetails(e.target.value); + }} + /> + + + + +

Prometheus data location

+
+ + + + <> + +

Provide the Prometheus URI endpoint to connect to.

+
+ { + setStore(e.target.value); + }} + onBlur={(e) => { + setStoreForRequest(e.target.value); + }} + /> + +
+ + + +

Authentication details

+
+ + + + { + setAuthMethodForRequest(e.target.value as AuthMethod); + }} + /> + + + + + + + +
+
+
+ ); +}; diff --git a/public/components/datasources/components/new/configure_s3_datasource.tsx b/public/components/datasources/components/new/configure_s3_datasource.tsx new file mode 100644 index 000000000..212bda33c --- /dev/null +++ b/public/components/datasources/components/new/configure_s3_datasource.tsx @@ -0,0 +1,229 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiLink, + EuiFormRow, + EuiFieldText, + EuiTextArea, + EuiSelect, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { + AuthMethod, + OPENSEARCH_DOCUMENTATION_URL, +} from '../../../../../common/constants/data_connections'; +import { QueryPermissionsConfiguration } from './query_permissions'; +import { Role } from '../../../../../common/types/data_connections'; +import { AuthDetails } from './auth_details'; + +interface ConfigureS3DatasourceProps { + roles: Role[]; + selectedQueryPermissionRoles: Role[]; + setSelectedQueryPermissionRoles: React.Dispatch>; + currentName: string; + currentDetails: string; + currentArn: string; + currentStore: string; + currentAuthMethod: AuthMethod; + currentUsername: string; + currentPassword: string; + setAuthMethodForRequest: React.Dispatch>; + setPasswordForRequest: React.Dispatch>; + setUsernameForRequest: React.Dispatch>; + setStoreForRequest: React.Dispatch>; + setNameForRequest: React.Dispatch>; + setDetailsForRequest: React.Dispatch>; + setArnForRequest: React.Dispatch>; +} + +export const ConfigureS3Datasource = (props: ConfigureS3DatasourceProps) => { + const { + setNameForRequest, + setDetailsForRequest, + setArnForRequest, + setStoreForRequest, + currentStore, + currentName, + currentDetails, + currentArn, + roles, + currentAuthMethod, + setAuthMethodForRequest, + selectedQueryPermissionRoles, + setSelectedQueryPermissionRoles, + currentPassword, + currentUsername, + setPasswordForRequest, + setUsernameForRequest, + } = props; + + const [name, setName] = useState(currentName); + const [details, setDetails] = useState(currentDetails); + const [arn, setArn] = useState(currentArn); + const [store, setStore] = useState(currentStore); + const authOptions = [ + { value: 'basicauth', text: 'Basic authentication' }, + { value: 'noauth', text: 'No authentication' }, + ]; + + return ( +
+ + +

{`Configure S3 Data Source`}

+
+ + + {`Connect to S3with OpenSearch and OpenSearch Dashboards `} + + Learn more + + + + +

Data source details

+
+ + + <> + +

+ This is the name the connection will be referenced by in OpenSearch Dashboards. It + is recommended to make this short yet descriptive to help users when selecting a + connection. +

+
+ { + setName(e.target.value); + }} + onBlur={(e) => { + setNameForRequest(e.target.value); + }} + /> + +
+ + { + setDetailsForRequest(e.target.value); + }} + onChange={(e) => { + setDetails(e.target.value); + }} + /> + + + + +

Glue authentication details

+
+ + + + <> + +

+ This parameters provides the authentication type information required for execution + engine to connect to glue. +

+
+ + +
+ + + <> + +

This should be the IAM role ARN

+
+ { + setArn(e.target.value); + }} + onBlur={(e) => { + setArnForRequest(e.target.value); + }} + /> + +
+ + + + +

Glue index store details

+
+ + + + <> + +

+ This parameters provides the OpenSearch cluster host information for glue. This + OpenSearch instance is used for writing index data back. +

+
+ { + setStore(e.target.value); + }} + onBlur={(e) => { + setStoreForRequest(e.target.value); + }} + /> + +
+ + + <> + +

Authentication settings to access the index store.

+
+ { + setAuthMethodForRequest(e.target.value as AuthMethod); + }} + /> + +
+ + + + + +
+
+ ); +}; diff --git a/public/components/datasources/components/new/new_datasource.tsx b/public/components/datasources/components/new/new_datasource.tsx new file mode 100644 index 000000000..ec06984dd --- /dev/null +++ b/public/components/datasources/components/new/new_datasource.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiPage, EuiPageBody } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { DataConnectionsHeader } from '../data_connections_header'; +import { HomeProps } from '../../home'; +import { NewDatasourceCardView } from './new_datasource_card_view'; + +export const NewDatasource = (props: HomeProps) => { + const { chrome } = props; + + useEffect(() => { + chrome.setBreadcrumbs([ + { + text: 'Data sources', + href: '#/', + }, + ]); + }, []); + + return ( + + + + + + + ); +}; diff --git a/public/components/datasources/components/new/new_datasource_card_view.tsx b/public/components/datasources/components/new/new_datasource_card_view.tsx new file mode 100644 index 000000000..66bb3612c --- /dev/null +++ b/public/components/datasources/components/new/new_datasource_card_view.tsx @@ -0,0 +1,69 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiPanel, EuiCard, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiIcon } from '@elastic/eui'; +import React from 'react'; +import { NewDatasourceDescription } from './new_datasource_description'; +import s3Svg from '../../icons/s3-logo.svg'; +import prometheusSvg from '../../icons/prometheus-logo.svg'; +import { DatasourceType } from '../../../../../common/types/data_connections'; + +export interface DatasourceCard { + name: DatasourceType; + displayName: string; + description: string; + displayIcon: JSX.Element; + onClick: () => void; +} + +export function NewDatasourceCardView() { + const Datasources: DatasourceCard[] = [ + { + name: 'S3GLUE', + displayName: 'S3', + description: 'Connect to Amazon S3 via Amazon Glue', + displayIcon: , + onClick: () => (window.location.hash = `#/configure/S3GLUE`), + }, + { + name: 'PROMETHEUS', + displayName: 'Prometheus', + description: 'Connect to Prometheus', + displayIcon: , + onClick: () => (window.location.hash = `#/configure/PROMETHEUS`), + }, + ]; + + const renderRows = (datasources: DatasourceCard[]) => { + return ( + <> + + {datasources.map((i, v) => { + return ( + + + + ); + })} + + + + ); + }; + + return ( + + + {renderRows(Datasources)} + + ); +} diff --git a/public/components/datasources/components/new/new_datasource_description.tsx b/public/components/datasources/components/new/new_datasource_description.tsx new file mode 100644 index 000000000..91f13c666 --- /dev/null +++ b/public/components/datasources/components/new/new_datasource_description.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiSpacer, EuiText, EuiTitle, EuiHorizontalRule } from '@elastic/eui'; +import React from 'react'; + +export const NewDatasourceDescription = () => { + return ( +
+ +

Create a new data source

+
+ + + + Connect to a compatible data source or compute engine to bring your data into OpenSearch and + OpenSearch Dashboards. + + +
+ ); +}; diff --git a/public/components/datasources/components/new/query_permissions.tsx b/public/components/datasources/components/new/query_permissions.tsx new file mode 100644 index 000000000..d8537d68a --- /dev/null +++ b/public/components/datasources/components/new/query_permissions.tsx @@ -0,0 +1,89 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiRadioGroup, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { + OPENSEARCH_DOCUMENTATION_URL, + QUERY_ALL, + QUERY_RESTRICTED, +} from '../../../../../common/constants/data_connections'; +import { PermissionsConfigurationProps } from '../../../../../common/types/data_connections'; + +export const QueryPermissionsConfiguration = (props: PermissionsConfigurationProps) => { + const { roles, selectedRoles, setSelectedRoles, layout } = props; + + const [selectedAccessLevel, setSelectedAccessLevel] = useState( + selectedRoles.length ? QUERY_RESTRICTED : QUERY_ALL + ); + const accessLevelOptions = [ + { + id: QUERY_RESTRICTED, + label: 'Restricted - accessible by users with specific OpenSearch roles', + }, + { + id: QUERY_ALL, + label: 'Everyone - accessible by all users on this cluster', + }, + ]; + + const ConfigureRoles = () => { + return ( +
+ + OpenSearch Roles + + Select one or more OpenSearch roles that can query this data connection. + + +
+ ); + }; + + return ( + + + + +

Query permissions

+
+ +

+ Control which OpenSearch roles have permission to query and index data from this data + source +

+
+
+ + setSelectedAccessLevel(id)} + name="query-radio-group" + legend={{ + children: Query access level, + }} + /> + {selectedAccessLevel === QUERY_RESTRICTED && } + +
+
+ ); +}; diff --git a/public/components/datasources/components/new/review_prometheus_datasource_configuration.tsx b/public/components/datasources/components/new/review_prometheus_datasource_configuration.tsx new file mode 100644 index 000000000..4b2d3788e --- /dev/null +++ b/public/components/datasources/components/new/review_prometheus_datasource_configuration.tsx @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiHorizontalRule, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import React from 'react'; +import { AuthMethod } from '../../../../../common/constants/data_connections'; +import { Role } from '../../../../../common/types/data_connections'; + +interface ConfigurePrometheusDatasourceProps { + selectedQueryPermissionRoles: Role[]; + currentName: string; + currentDetails: string; + currentArn: string; + currentStore: string; + currentUsername: string; + currentAuthMethod: AuthMethod; + goBack: () => void; +} + +export const ReviewPrometheusDatasource = (props: ConfigurePrometheusDatasourceProps) => { + const { + currentStore, + currentName, + currentDetails, + currentAuthMethod, + selectedQueryPermissionRoles, + goBack, + } = props; + + return ( +
+ + +

{`Review Prometheus Data Source Configuration`}

+
+ + + + + +

Data source configuration

+
+
+ + Edit + +
+ + + + + + + Data source name + + {currentName} + + + + Description + + {currentDetails} + + + + + + + + Prometheus URI + + {currentStore} + + + + Authentication method + + {currentAuthMethod === 'basicauth' + ? 'Basic authentication' + : 'AWS Signature Version 4'} + + + + + + + + Query Permissions + + {selectedQueryPermissionRoles && selectedQueryPermissionRoles.length + ? `Restricted - ${selectedQueryPermissionRoles + .map((role) => role.label) + .join(',')}` + : 'Everyone'} + + + + + +
+
+ ); +}; diff --git a/public/components/datasources/components/new/review_s3_datasource_configuration.tsx b/public/components/datasources/components/new/review_s3_datasource_configuration.tsx new file mode 100644 index 000000000..419c66e01 --- /dev/null +++ b/public/components/datasources/components/new/review_s3_datasource_configuration.tsx @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiHorizontalRule, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import React from 'react'; +import { AuthMethod } from '../../../../../common/constants/data_connections'; +import { Role } from '../../../../../common/types/data_connections'; + +interface ConfigureS3DatasourceProps { + selectedQueryPermissionRoles: Role[]; + currentName: string; + currentDetails: string; + currentArn: string; + currentStore: string; + currentAuthMethod: AuthMethod; + goBack: () => void; +} + +export const ReviewS3Datasource = (props: ConfigureS3DatasourceProps) => { + const { + currentStore, + currentName, + currentDetails, + currentArn, + selectedQueryPermissionRoles, + currentAuthMethod, + goBack, + } = props; + + return ( +
+ + +

{`Review S3 Data Source Configuration`}

+
+ + + + + +

Data source configuration

+
+
+ + Edit + +
+ + + + + + + Data source name + + {currentName} + + + + Description + + {currentDetails} + + + + + + + + Glue authentication ARN + + {currentArn} + + + + Glue index store URI + + {currentStore} + + + + + + + + Query Permissions + + {selectedQueryPermissionRoles && selectedQueryPermissionRoles.length + ? `Restricted - ${selectedQueryPermissionRoles + .map((role) => role.label) + .join(',')}` + : 'Everyone'} + + + + Authentication method + + {currentAuthMethod === 'basicauth' ? 'Basic authentication' : 'No authentication'} + + + + + +
+
+ ); +}; diff --git a/public/components/datasources/components/no_access.tsx b/public/components/datasources/components/no_access.tsx new file mode 100644 index 000000000..1766f7fe1 --- /dev/null +++ b/public/components/datasources/components/no_access.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButton, EuiEmptyPrompt, EuiPage, EuiText } from '@elastic/eui'; +import React from 'react'; + +export const NoAccess = () => { + return ( + + {'No permissions to access'}} + body={ + + { + 'You are missing permissions to view connection details. Contact your administrator for permissions.' + } + + } + actions={ + (window.location.hash = '')}> + Return to data connections + + } + /> + + ); +}; diff --git a/public/components/datasources/components/save_or_cancel.tsx b/public/components/datasources/components/save_or_cancel.tsx new file mode 100644 index 000000000..ccb966210 --- /dev/null +++ b/public/components/datasources/components/save_or_cancel.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiBottomBar, EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +interface SaveOrCancelProps { + onSave: () => void; + onCancel: () => void; +} + +export const SaveOrCancel = (props: SaveOrCancelProps) => { + const { onSave, onCancel } = props; + return ( + + + + + Discard change(s) + + + + + Save + + + + + ); +}; diff --git a/public/components/datasources/home.tsx b/public/components/datasources/home.tsx new file mode 100644 index 000000000..0bbfa93cd --- /dev/null +++ b/public/components/datasources/home.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { + ChromeBreadcrumb, + ChromeStart, + HttpStart, + NotificationsStart, +} from '../../../../../src/core/public'; +import { DataConnection } from './components/manage/data_connection'; +import { ManageDataConnectionsTable } from './components/manage/manage_data_connections_table'; +import { NewDatasource } from './components/new/new_datasource'; +import { Configure } from './components/new/configure_datasource'; + +export interface HomeProps extends RouteComponentProps { + pplService: any; + parentBreadcrumb: ChromeBreadcrumb; + http: HttpStart; + chrome: ChromeStart; + notifications: NotificationsStart; +} + +export const Home = (props: HomeProps) => { + const { http, chrome, pplService, notifications } = props; + + const commonProps = { + http, + chrome, + pplService, + notifications, + }; + + return ( + + + ( + + )} + /> + + } + /> + } /> + + ( + + )} + /> + + + ); +}; diff --git a/public/components/datasources/icons/prometheus-logo.svg b/public/components/datasources/icons/prometheus-logo.svg new file mode 100644 index 000000000..e21c6a7e2 --- /dev/null +++ b/public/components/datasources/icons/prometheus-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/components/datasources/icons/s3-logo.svg b/public/components/datasources/icons/s3-logo.svg new file mode 100644 index 000000000..5b0c3a35a --- /dev/null +++ b/public/components/datasources/icons/s3-logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap index 540037021..a0e63ed20 100644 --- a/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap +++ b/public/components/event_analytics/explorer/__tests__/__snapshots__/data_grid.test.tsx.snap @@ -1,699 +1,436 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Datagrid component Renders data grid component 1`] = ` - + -
- - - - - - - - - - - - - - - - - - + - - - - - -
- - double_per_ip_bytes - - host - - ip_count - - per_ip_bytes - - resp_code - - sum_bytes -
- - - -
- - - -
-
-
- +
+
+ +
+ + + + + +
+ + `; diff --git a/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx b/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx index f659f3243..5a1729adf 100644 --- a/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx +++ b/public/components/event_analytics/explorer/__tests__/data_grid.test.tsx @@ -8,17 +8,27 @@ import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { waitFor } from '@testing-library/react'; import { DataGrid } from '../events_views/data_grid'; -import { - SELECTED_FIELDS, +import { + SELECTED_FIELDS, AVAILABLE_FIELDS, UNSELECTED_FIELDS, - QUERIED_FIELDS + QUERIED_FIELDS, + DEFAULT_EMPTY_EXPLORER_FIELDS, } from '../../../../../common/constants/explorer'; -import { +import { AVAILABLE_FIELDS as SIDEBAR_AVAILABLE_FIELDS, QUERY_FIELDS, - DATA_GRID_ROWS + DATA_GRID_ROWS, + EXPLORER_DATA_GRID_QUERY, } from '../../../../../test/event_analytics_constants'; +import httpClientMock from '../../../../../test/__mocks__/httpClientMock'; +import { sampleEmptyPanel } from '../../../../../test/panels_constants'; +import { HttpResponse } from '../../../../../../../src/core/public'; +import PPLService from '../../../../../public/services/requests/ppl'; +import { applyMiddleware, createStore } from 'redux'; +import { rootReducer } from '../../../../../public/framework/redux/reducers'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; describe('Datagrid component', () => { configure({ adapter: new Adapter() }); @@ -28,21 +38,42 @@ describe('Datagrid component', () => { [SELECTED_FIELDS]: [], [UNSELECTED_FIELDS]: [], [AVAILABLE_FIELDS]: SIDEBAR_AVAILABLE_FIELDS, - [QUERIED_FIELDS]: QUERY_FIELDS + [QUERIED_FIELDS]: QUERY_FIELDS, }; - + + httpClientMock.get = jest.fn(() => + Promise.resolve((sampleEmptyPanel as unknown) as HttpResponse) + ); + + const http = httpClientMock; + const pplService = new PPLService(httpClientMock); + const store = createStore(rootReducer, applyMiddleware(thunk)); + const wrapper = mount( - + + + ); - + wrapper.update(); await waitFor(() => { expect(wrapper).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); diff --git a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap deleted file mode 100644 index 091744a7a..000000000 --- a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/doc_viewer_row.test.tsx.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Datagrid Doc viewer row component Renders Doc viewer row component 1`] = ` - - - - - - - - - 45.957544288332315 - - - -`; diff --git a/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap new file mode 100644 index 000000000..be38d974b --- /dev/null +++ b/public/components/event_analytics/explorer/events_views/__tests__/__snapshots__/flyout_button.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Datagrid Doc viewer row component Renders Doc viewer row component 1`] = ` + + + + + +`; diff --git a/public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx b/public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx similarity index 60% rename from public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx rename to public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx index 684ede48c..079a93842 100644 --- a/public/components/event_analytics/explorer/events_views/__tests__/doc_viewer_row.test.tsx +++ b/public/components/event_analytics/explorer/events_views/__tests__/flyout_button.test.tsx @@ -7,33 +7,29 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { waitFor } from '@testing-library/react'; -import { DocViewRow } from '../docViewRow'; +import { FlyoutButton } from '../docViewRow'; describe('Datagrid Doc viewer row component', () => { configure({ adapter: new Adapter() }); it('Renders Doc viewer row component', async () => { - const hit = { - 'Carrier': 'JetBeats', - 'avg(FlightDelayMin)': '45.957544288332315' + Carrier: 'JetBeats', + 'avg(FlightDelayMin)': '45.957544288332315', }; - const selectedCols = [{ - name: 'avg(FlightDelayMin)', - type: 'double' - }]; + const selectedCols = [ + { + name: 'avg(FlightDelayMin)', + type: 'double', + }, + ]; + + const wrapper = mount(); - const wrapper = mount( - - ); - wrapper.update(); await waitFor(() => { expect(wrapper).toMatchSnapshot(); }); }); -}); \ No newline at end of file +}); diff --git a/public/components/event_analytics/explorer/events_views/data_grid.scss b/public/components/event_analytics/explorer/events_views/data_grid.scss index f0dcbfbbb..bf5392d3b 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.scss +++ b/public/components/event_analytics/explorer/events_views/data_grid.scss @@ -488,4 +488,8 @@ .osdDocViewer__warning { margin-right: $euiSizeS; } + + .euiDescriptionList.euiDescriptionList--inline .euiDescriptionList__title.osdDescriptionListFieldTitle { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); + } } diff --git a/public/components/event_analytics/explorer/events_views/data_grid.tsx b/public/components/event_analytics/explorer/events_views/data_grid.tsx index fb15d0694..8ac9c7aa9 100644 --- a/public/components/event_analytics/explorer/events_views/data_grid.tsx +++ b/public/components/event_analytics/explorer/events_views/data_grid.tsx @@ -3,148 +3,251 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState, useEffect, useRef, RefObject } from 'react'; -import { IExplorerFields } from '../../../../../common/types/explorer'; -import { DEFAULT_COLUMNS, PAGE_SIZE } from '../../../../../common/constants/explorer'; -import { getHeaders, getTrs, populateDataGrid } from '../../utils'; +import React, { useMemo, useState, useRef, RefObject, Fragment, useCallback } from 'react'; +import { + EuiDataGrid, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiDataGridColumn, + EuiDataGridSorting, +} from '@elastic/eui'; +import moment from 'moment'; +import dompurify from 'dompurify'; +import datemath from '@elastic/datemath'; +import { MutableRefObject } from 'react'; +import { GridSortingColumn, IExplorerFields, IField } from '../../../../../common/types/explorer'; +import { + DATE_DISPLAY_FORMAT, + DATE_PICKER_FORMAT, + DEFAULT_SOURCE_COLUMN, + DEFAULT_TIMESTAMP_COLUMN, +} from '../../../../../common/constants/explorer'; import { HttpSetup } from '../../../../../../../src/core/public'; import PPLService from '../../../../services/requests/ppl'; +import { FlyoutButton, IDocType } from './docViewRow'; +import { useFetchEvents } from '../../hooks'; +import { + PPL_INDEX_INSERT_POINT_REGEX, + PPL_NEWLINE_REGEX, +} from '../../../../../common/constants/shared'; +import { redoQuery } from '../../utils/utils'; interface DataGridProps { http: HttpSetup; pplService: PPLService; - rows: Array; - rowsAll: Array; + rows: any[]; + rowsAll: any[]; explorerFields: IExplorerFields; timeStampField: string; rawQuery: string; + totalHits: number; + requestParams: any; + startTime: string; + endTime: string; + storedSelectedColumns: IField[]; } export function DataGrid(props: DataGridProps) { - const { http, pplService, rows, rowsAll, explorerFields, timeStampField, rawQuery } = props; - const [limit, setLimit] = useState(PAGE_SIZE); - const loader = useRef(null); - const [rowRefs, setRowRefs] = useState[]>([]); + const { + http, + pplService, + rows, + rowsAll, + explorerFields, + timeStampField, + rawQuery, + totalHits, + requestParams, + startTime, + endTime, + storedSelectedColumns, + } = props; + const { getEvents } = useFetchEvents({ + pplService, + requestParams, + }); + // useRef instead of useState somehow solves the issue of user triggered sorting not + // having any delays + const sortingFields: MutableRefObject = useRef([]); + const pageFields = useRef([0, 100]); - useEffect(() => { - if (!loader.current) return; + // setSort and setPage are used to change the query and send a direct request to get data + const setSort = (sort: EuiDataGridSorting['columns']) => { + sortingFields.current = sort; + redoQuery(startTime, endTime, rawQuery, timeStampField, sortingFields, pageFields, getEvents); + }; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) setLimit((limit) => limit + PAGE_SIZE); - }, - { - root: null, - rootMargin: '500px', - threshold: 0, - } - ); - observer.observe(loader.current); + const setPage = (page: number[]) => { + pageFields.current = page; + redoQuery(startTime, endTime, rawQuery, timeStampField, sortingFields, pageFields, getEvents); + }; - return () => observer.disconnect(); - }, [loader]); + // creates the header for each column listing what that column is + const dataGridColumns = useMemo(() => { + if (storedSelectedColumns.length > 0) { + const columns: EuiDataGridColumn[] = []; + storedSelectedColumns.map(({ name, type }) => { + if (name === 'timestamp') { + columns.push(DEFAULT_TIMESTAMP_COLUMN); + } else if (name === '_source') { + columns.push(DEFAULT_SOURCE_COLUMN); + } else { + columns.push({ + id: name, + display: name, + isSortable: true, // TODO: add functionality here based on type + }); + } + }); + return columns; + } + return []; + }, [storedSelectedColumns]); - const onFlyoutOpen = (docId: string) => { - rowRefs.forEach((rowRef) => { - rowRef.current?.closeAllFlyouts(docId); - }); - }; + // used for which columns are visible and their order + const dataGridColumnVisibility = useMemo(() => { + if (storedSelectedColumns.length > 0) { + const columns: string[] = []; + storedSelectedColumns.map(({ name }) => { + columns.push(name); + }); + return { + visibleColumns: columns, + setVisibleColumns: (visibleColumns: string[]) => { + // TODO: implement with sidebar field order (dragability) changes + }, + }; + } + // default shown fields + throw new Error('explorer data grid stored columns empty'); + }, [storedSelectedColumns]); + + // sets the very first column, which is the button used for the flyout of each row + const dataGridLeadingColumns = useMemo(() => { + return [ + { + id: 'inspectCollapseColumn', + headerCellRender: () => null, + rowCellRender: ({ rowIndex }: { rowIndex: number }) => { + return ( + {}} + /> + ); + }, + width: 40, + }, + ]; + }, [rows, http, explorerFields, pplService, rawQuery, timeStampField]); + + // renders what is shown in each cell, i.e. the content of each row + const dataGridCellRender = useCallback( + ({ rowIndex, columnId }: { rowIndex: number; columnId: string }) => { + const trueIndex = rowIndex % pageFields.current[1]; + if (trueIndex < rows.length) { + if (columnId === '_source') { + return ( + + {Object.keys(rows[trueIndex]).map((key) => ( + + + {key} + + + {rows[trueIndex][key]} + + + ))} + + ); + } + if (columnId === 'timestamp') { + return `${moment(rows[trueIndex][columnId]).format(DATE_DISPLAY_FORMAT)}`; + } + return `${rows[trueIndex][columnId]}`; + } + return null; + }, + [rows, pageFields, explorerFields] + ); - const Queriedheaders = useMemo(() => getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS), [ - explorerFields.queriedFields, - ]); - const [QueriedtableRows, setQueriedtableRows] = useState([]); - useEffect(() => { - setQueriedtableRows( - getTrs( - http, - explorerFields.queriedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - rows - ) - ); - }, [rows, explorerFields.queriedFields]); + // ** Pagination config + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 100 }); + // changing the number of items per page, reset index and modify page size + const onChangeItemsPerPage = useCallback( + (pageSize) => + setPagination(() => { + setPage([0, pageSize]); + return { pageIndex: 0, pageSize }; + }), + [setPagination, setPage] + ); + // changing the page index, keep page size constant + const onChangePage = useCallback( + (pageIndex) => { + setPagination(({ pageSize }) => { + setPage([pageIndex, pageSize]); + return { pageSize, pageIndex }; + }); + }, + [setPagination, setPage] + ); - const headers = useMemo(() => getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS), [ - explorerFields.selectedFields, - ]); - const [tableRows, setTableRows] = useState([]); - useEffect(() => { - const dataToRender = - explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 ? rowsAll : rows; - setTableRows( - getTrs( - http, - explorerFields.selectedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - dataToRender - ) - ); - }, [rows, explorerFields.selectedFields]); + const rowHeightsOptions = useMemo( + () => ({ + defaultHeight: { + // if source is listed as a column, add extra space + lineCount: storedSelectedColumns.some((obj) => obj.name === '_source') ? 3 : 1, + }, + }), + [storedSelectedColumns] + ); - useEffect(() => { - setQueriedtableRows((prev) => - getTrs( - http, - explorerFields.queriedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - rows, - prev - ) - ); - const dataToRender = - explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 ? rowsAll : rows; - setTableRows((prev) => - getTrs( - http, - explorerFields.selectedFields, - limit, - setLimit, - PAGE_SIZE, - timeStampField, - explorerFields, - pplService, - rawQuery, - rowRefs, - setRowRefs, - onFlyoutOpen, - dataToRender, - prev - ) - ); - }, [limit]); + // TODO: memoize the expensive table below return ( <> - {populateDataGrid(explorerFields, Queriedheaders, QueriedtableRows, headers, tableRows)} -
+
+ +
); } diff --git a/public/components/event_analytics/explorer/events_views/docViewRow.tsx b/public/components/event_analytics/explorer/events_views/docViewRow.tsx index b48bd6f96..bc66e0abf 100644 --- a/public/components/event_analytics/explorer/events_views/docViewRow.tsx +++ b/public/components/event_analytics/explorer/events_views/docViewRow.tsx @@ -24,7 +24,7 @@ export interface IDocType { [key: string]: string; } -interface IDocViewRowProps { +interface FlyoutButtonProps { http: HttpStart; doc: IDocType; docId: string; @@ -36,7 +36,7 @@ interface IDocViewRowProps { onFlyoutOpen: (docId: string) => void; } -export const DocViewRow = forwardRef((props: IDocViewRowProps, ref) => { +export const FlyoutButton = forwardRef((props: FlyoutButtonProps, ref) => { const { http, doc, @@ -271,15 +271,11 @@ export const DocViewRow = forwardRef((props: IDocViewRowProps, ref) => { return ( <> - - {memorizedTds} - + toggleDetailOpen()} + iconType={'inspect'} + aria-label="inspect document details" + /> {flyout} ); diff --git a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx index e7183cc4a..dc1629b71 100644 --- a/public/components/event_analytics/explorer/events_views/doc_flyout.tsx +++ b/public/components/event_analytics/explorer/events_views/doc_flyout.tsx @@ -124,13 +124,6 @@ export const DocFlyout = ({ const flyoutBody = (
- {populateDataGrid( - explorerFields, - getHeaders(explorerFields.queriedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds}, - getHeaders(explorerFields.selectedFields, DEFAULT_COLUMNS.slice(1), true), - {memorizedTds} - )}
diff --git a/public/components/event_analytics/explorer/log_explorer.tsx b/public/components/event_analytics/explorer/log_explorer.tsx index f7f93d8c7..78c0e90b4 100644 --- a/public/components/event_analytics/explorer/log_explorer.tsx +++ b/public/components/event_analytics/explorer/log_explorer.tsx @@ -3,36 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ /* eslint-disable react-hooks/exhaustive-deps */ - -import { - EuiIcon, - EuiTabbedContent, - EuiTabbedContentTab, - EuiText, - htmlIdGenerator, -} from '@elastic/eui'; -import $ from 'jquery'; -import { isEmpty, map } from 'lodash'; -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { isEmpty } from 'lodash'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { LogExplorerRouterContext } from '..'; import { APP_ANALYTICS_TAB_ID_REGEX, CREATE_TAB_PARAM_KEY, - NEW_TAB, - REDIRECT_TAB, - SAVED_OBJECT_ID, TAB_CHART_ID, TAB_EVENT_ID, - TAB_ID_TXT_PFX, - TAB_TITLE, } from '../../../../common/constants/explorer'; import { initializeTabData, removeTabData } from '../../application_analytics/helpers/utils'; import { EmptyTabParams, ILogExplorerProps } from '../../../../common/types/explorer'; import { selectQueryResult } from '../redux/slices/query_result_slice'; import { selectQueries } from '../redux/slices/query_slice'; -import { selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; +import { selectQueryTabs } from '../redux/slices/query_tab_slice'; import { Explorer } from './explorer'; const searchBarConfigs = { @@ -62,11 +48,9 @@ export const LogExplorer = ({ }: ILogExplorerProps) => { const history = useHistory(); const routerContext = useContext(LogExplorerRouterContext); - const dispatch = useDispatch(); const tabIds = useSelector(selectQueryTabs).queryTabIds.filter( (tabid: string) => !tabid.match(APP_ANALYTICS_TAB_ID_REGEX) ); - const tabNames = useSelector(selectQueryTabs).tabNames; const queries = useSelector(selectQueries); const curSelectedTabId = useSelector(selectQueryTabs).selectedQueryTab; const explorerData = useSelector(selectQueryResult); @@ -81,68 +65,12 @@ export const LogExplorer = ({ const [tabCreatedTypes, setTabCreatedTypes] = useState({}); - // Append add-new-tab link to the end of the tab list, and remove it once tabs state changes - useEffect(() => { - const newLink = $( - '+ Add new' - ).on('click', () => { - addNewTab(NEW_TAB); - }); - $('.queryTabs > .euiTabs').append(newLink); - return () => { - $('.queryTabs > .euiTabs .linkNewTag').remove(); - }; - }, [tabIds]); - - const handleTabClick = (selectedTab: EuiTabbedContentTab) => { - history.replace(`/explorer/${queryRef.current![selectedTab.id][SAVED_OBJECT_ID] || ''}`); - dispatch(setSelectedQueryTab({ tabId: selectedTab.id })); - }; - - const handleTabClose = (TabIdToBeClosed: string) => { - if (tabIds.length === 1) { - setToast('Cannot close last tab.', 'danger'); - return; - } - - const index: number = tabIds.indexOf(TabIdToBeClosed); - const curSelectedTab = curSelectedTabIdRef.current; - let newIdToFocus = ''; - if (TabIdToBeClosed === curSelectedTab) { - if (index === 0) { - newIdToFocus = tabIds[index + 1]; - } else if (index > 0) { - newIdToFocus = tabIds[index - 1]; - } - } - removeTabData(dispatch, TabIdToBeClosed, newIdToFocus); - }; - - const addNewTab = async (where: string) => { - // get a new tabId - const tabId = htmlIdGenerator(TAB_ID_TXT_PFX)(); - - // create a new tab - await initializeTabData(dispatch, tabId, where); - - setTabCreatedTypes((staleState) => { - return { - ...staleState, - [tabId]: where, - }; - }); - - return tabId; - }; - const dispatchSavedObjectId = async () => { - const emptyTabId = getExistingEmptyTab({ + return getExistingEmptyTab({ tabIds: tabIdsRef.current, queries: queryRef.current, explorerData: explorerDataRef.current, }); - const newTabId = emptyTabId ? emptyTabId : await addNewTab(REDIRECT_TAB); - return newTabId; }; useEffect(() => { @@ -152,7 +80,6 @@ export const LogExplorer = ({ if (routerContext && routerContext.searchParams.has(CREATE_TAB_PARAM_KEY)) { // need to wait for current redux event loop to finish setImmediate(() => { - addNewTab(NEW_TAB); routerContext.searchParams.delete(CREATE_TAB_PARAM_KEY); routerContext.routerProps.history.replace({ search: routerContext.searchParams.toString(), @@ -161,68 +88,6 @@ export const LogExplorer = ({ } }, []); - function getQueryTab({ - tabTitle, - tabId, - handlesTabClose, - }: { - tabTitle: string; - tabId: string; - handlesTabClose: (TabIdToBeClosed: string) => void; - }) { - return { - id: tabId, - name: ( - <> - - {tabTitle} - { - e.stopPropagation(); - handlesTabClose(tabId); - }} - data-test-subj="eventExplorer__tabClose" - /> - - - ), - content: ( - <> - - - ), - }; - } - - const memorizedTabs = useMemo(() => { - const res = map(tabIds, (tabId) => { - return getQueryTab({ - tabTitle: tabNames[tabId] || TAB_TITLE, - tabId, - handlesTabClose: handleTabClose, - }); - }); - - return res; - }, [tabIds, tabNames, tabCreatedTypes]); - return ( <> @@ -34,7 +34,7 @@ exports[`Field component Renders a sidebar field 1`] = ` + @@ -82,12 +86,16 @@ exports[`Field component Renders a sidebar field 1`] = ` data-test-subj="field-agent" title="agent" > - agent + + agent + } isActive={false} onClick={[Function]} - size="m" + size="s" /> } closePopover={[Function]} @@ -95,7 +103,7 @@ exports[`Field component Renders a sidebar field 1`] = ` hasArrow={true} isOpen={false} ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" + panelClassName="explorerSidebarItem__fieldPopoverPanel" panelPaddingSize="m" >
@@ -118,7 +126,7 @@ exports[`Field component Renders a sidebar field 1`] = ` + @@ -166,15 +178,19 @@ exports[`Field component Renders a sidebar field 1`] = ` data-test-subj="field-agent" title="agent" > - agent + + agent + } isActive={false} onClick={[Function]} - size="m" + size="s" >
@@ -260,7 +284,7 @@ exports[`Field component Renders a sidebar field 1`] = ` > + + +
diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap index bc4bb3265..f42787fa5 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap +++ b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -113,107 +113,162 @@ exports[`Siderbar component Renders empty sidebar component 1`] = ` textComponent={Symbol(react.fragment)} > -
-
- - + -
-
- - + - - -
- - - - - -
-
-
-
-
-
-
- -
- -
-
+ + +
+ + + + + +
+
+
+
+ + +
+ +
+ +
+ + + + + + @@ -490,6 +545,120 @@ exports[`Siderbar component Renders sidebar component 1`] = ` isFieldToggleButtonDisabled={false} isOverridingTimestamp={false} selectedTimestamp="timestamp" + storedExplorerFields={ + Object { + "availableFields": Array [ + Object { + "name": "agent", + "type": "string", + }, + Object { + "name": "bytes", + "type": "long", + }, + Object { + "name": "clientip", + "type": "ip", + }, + Object { + "name": "event", + "type": "struct", + }, + Object { + "name": "extension", + "type": "string", + }, + Object { + "name": "geo", + "type": "struct", + }, + Object { + "name": "host", + "type": "string", + }, + Object { + "name": "index", + "type": "string", + }, + Object { + "name": "ip", + "type": "ip", + }, + Object { + "name": "machine", + "type": "struct", + }, + Object { + "name": "memory", + "type": "double", + }, + Object { + "name": "message", + "type": "string", + }, + Object { + "name": "phpmemory", + "type": "long", + }, + Object { + "name": "referer", + "type": "string", + }, + Object { + "name": "request", + "type": "string", + }, + Object { + "name": "response", + "type": "string", + }, + Object { + "name": "tags", + "type": "string", + }, + Object { + "name": "timestamp", + "type": "timestamp", + }, + Object { + "name": "url", + "type": "string", + }, + Object { + "name": "utc_time", + "type": "timestamp", + }, + ], + "queriedFields": Array [ + Object { + "name": "double_per_ip_bytes", + "type": "long", + }, + Object { + "name": "host", + "type": "text", + }, + Object { + "name": "ip_count", + "type": "integer", + }, + Object { + "name": "per_ip_bytes", + "type": "long", + }, + Object { + "name": "resp_code", + "type": "text", + }, + Object { + "name": "sum_bytes", + "type": "long", + }, + ], + "selectedFields": Array [], + "unselectedFields": Array [], + } + } > -
-
- - + -
-
- - + - - -
- - - - - -
-
-
-
-
-
-
- -
- -
- - - Query fields - - - } - id="fieldSelector__queriedFields" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - paddingSize="xs" - > -
-
-
+ +
+
+ + +
+ - - - Query fields - - - - -
-
- -
+ +
+ + + Query fields + + + } + id="fieldSelector__queriedFields" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + paddingSize="xs" > -
    -
  • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - double_per_ip_bytes - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" + + + + + + + + + + + Query fields + + + + +
+
+ +
-
- - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - double_per_ip_bytes - - } - isActive={false} - onClick={[Function]} - size="m" +
+ + + -
- +
+ + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+ +
+ + + + + + + - double_per_ip_bytes - - - -
- - - - - - - - + + +
+ +
+ + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + host + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
+
+ + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + host + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
+ +
+ + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + - - - -
-
-
-
- -
-
- - - -
  • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - host - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - host - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + +
    + + + + + + + - - - - - - - - - - - - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + per_ip_bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + per_ip_bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - host - - - -
    - - - - - - - - + + +
    + +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + resp_code + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + resp_code + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + - - - -
    -
    + +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + sum_bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + sum_bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    + +
    -
    - -
    + + +
    - - - -
  • + +
  • +
    + + +
    + + + + Selected Fields + + + } + id="fieldSelector__selectedFields" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + paddingSize="xs" + > +
    +
    - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - ip_count - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" + + + + + + + + + + + Selected Fields + + + + +
    +
    + +
    -
    - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - ip_count - - } - isActive={false} - onClick={[Function]} - size="m" +
    + + + -
    -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    - - - -
  • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - per_ip_bytes - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - per_ip_bytes - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    -
  • -
  • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - resp_code - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - resp_code - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - -
    - - - - - - - - - - - - - -
    -
    -
    -
    -
    -
    -
    -
  • -
  • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - sum_bytes - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - sum_bytes - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - -
    - - - - - - - - - - - - - +
    -
    - -
    + + +
  • - - - - -
    -
    - -
    -
    - - -
    - - - - Selected Fields - - - } - id="fieldSelector__selectedFields" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - paddingSize="xs" - > -
    -
    - -
    -
    - -
    -
    -
      -
    -
    -
    -
    -
    -
    - -
    - - - - Available Fields - - - } - id="fieldSelector__availableFields" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - paddingSize="xs" - > -
    -
    -
    +
    +
    + - - Available Fields - - - - -
    -
    - -
    -
    -
      -
    • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - agent - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - agent - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - bytes - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - bytes - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - clientip - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - clientip - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - event - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - event - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - extension - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - extension - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - geo - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - geo - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - host - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - host - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - index - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - index - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - ip - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - ip - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - machine - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - machine - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - memory - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - memory - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - message - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
      -
      - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - message - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • + + + + Available Fields + + + } + id="fieldSelector__availableFields" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + paddingSize="xs" + > +
      +
      - - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - phpmemory - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" + -
      -
      - - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - phpmemory - - } - isActive={false} - onClick={[Function]} - size="m" - > -
      - -
      - - - - - - - - - - - - - -
      -
      -
      -
      -
      -
      -
      -
    • -
    • + + + + + + + Available Fields + + + + +
    +
    - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - referer - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > +
    -
    - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - referer - - } - isActive={false} - onClick={[Function]} - size="m" +
    + + + -
    - -
    - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + agent + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + agent + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - - - - - - - - - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + bytes + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - - - - - -
    -
    - -
    -
    - - - -
  • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - request - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - request - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + +
    + + + + + + + - - - - - - - - - - - - - - request - - - -
    - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + event + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + event + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - - - - - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + extension + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + extension + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - - - - - -
    -
    - -
    -
    - - - -
  • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - response - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - response - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + +
    + + + + + + + - - - - - - - - - - - - - - response - - - -
    - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + host + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + host + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - - - - - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + index + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + index + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - - - - - -
    -
    - -
    -
    - - - -
  • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - tags - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - tags - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + +
    + + + + + + + - - - - - - - - - - - - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + machine + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + machine + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + + + + + - tags - - - -
    - - - - - - - - - - - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + memory + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + memory + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - -
    -
    - -
    -
    - - - -
  • - - - - - - - - - Default Timestamp - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - timestamp - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - Default Timestamp - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - timestamp - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + +
    + + + + + + + - - - - - - - - - - - - - - timestamp - - - -
    - - - - - - - - Default Timestamp - - - - - - +
    + + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + phpmemory + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + phpmemory + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - -
    -
    - -
    -
    - - - -
  • - - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - url - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - Override - - - - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - url - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
  • + + + + + + + + + - - - - - - - - - - - - - - url - - - -
    - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + request + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + request + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - - - - - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + response + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + response + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    + + - - - - - - - - - - - -
    - -
  • - - - - - - - - - Override - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - utc_time - - } - isActive={false} - onClick={[Function]} - size="m" - /> - } - closePopover={[Function]} - display="block" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelClassName="dscSidebarItem__fieldPopoverPanel" - panelPaddingSize="m" - > -
    -
    - - - - - - - - Override - - - - - - - - - - } - fieldIcon={ - - } - fieldName={ - - utc_time - - } - isActive={false} - onClick={[Function]} - size="m" - > -
    - +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + - - - - - - - - - - - - - +
    + + + + + + + + + Default Timestamp + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + timestamp + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + Default Timestamp + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + timestamp + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + Default Timestamp + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + + + +
    +
    + + - utc_time - - - -
    - - - - - - - - - - - - +
    + + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + url + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + Override + + + + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + url + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    + + - - - - - + +
    + + + + + + + + + Override + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + utc_time + + + } + isActive={false} + onClick={[Function]} + size="s" + /> + } + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelClassName="explorerSidebarItem__fieldPopoverPanel" + panelPaddingSize="m" + > +
    +
    + + + + + + + + Override + + + + + + + + + + + } + fieldIcon={ + + } + fieldName={ + + + utc_time + + + } + isActive={false} + onClick={[Function]} + size="s" + > +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    + +
    - - - + + + - - -
  • - - + + + + + - - - - - - + + + + + + diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/sidebar.test.tsx b/public/components/event_analytics/explorer/sidebar/__tests__/sidebar.test.tsx index ae18768c1..c6d2e6bfd 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/sidebar.test.tsx +++ b/public/components/event_analytics/explorer/sidebar/__tests__/sidebar.test.tsx @@ -92,6 +92,7 @@ describe('Siderbar component', () => { handleOverrideTimestamp={handleOverrideTimestamp} isFieldToggleButtonDisabled={false} isOverridingTimestamp={false} + storedExplorerFields={explorerFields} /> ); diff --git a/public/components/event_analytics/explorer/sidebar/field.tsx b/public/components/event_analytics/explorer/sidebar/field.tsx index 2b18be672..09b23f636 100644 --- a/public/components/event_analytics/explorer/sidebar/field.tsx +++ b/public/components/event_analytics/explorer/sidebar/field.tsx @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, + EuiText, } from '@elastic/eui'; import { FieldButton } from '../../../common/field_button'; import { FieldIcon } from '../../../common/field_icon'; @@ -68,8 +69,8 @@ export const Field = (props: IFieldProps) => { setIsFieldDetailsOpen((staleState) => !staleState); }; - const toggleField = (field: IField) => { - onToggleField(field); + const toggleField = (fields: IField) => { + onToggleField(fields); }; const getFieldActionDOM = () => { @@ -83,11 +84,11 @@ export const Field = (props: IFieldProps) => { Default Pattern ) : isOverridingPattern ? ( - + ) : ( { Default Timestamp ) : isOverridingTimestamp ? ( - + ) : ( { <> {isFieldToggleButtonDisabled ? ( { ) => { if (e.type === 'click') { e.currentTarget.focus(); @@ -163,6 +164,7 @@ export const Field = (props: IFieldProps) => { aria-label={selected ? removeLabelAria : addLabelAria} /> )} + @@ -176,11 +178,11 @@ export const Field = (props: IFieldProps) => { isOpen={isFieldDetailsOpen} closePopover={() => setIsFieldDetailsOpen(false)} anchorPosition="rightUp" - panelClassName="dscSidebarItem__fieldPopoverPanel" + panelClassName="explorerSidebarItem__fieldPopoverPanel" button={ } @@ -190,7 +192,7 @@ export const Field = (props: IFieldProps) => { title={field.name} className="dscSidebarField__name" > - {field.name} + {field.name} } fieldAction={getFieldActionDOM()} diff --git a/public/components/event_analytics/explorer/sidebar/field_insights.tsx b/public/components/event_analytics/explorer/sidebar/field_insights.tsx index dd0f42330..2b82bfa0e 100644 --- a/public/components/event_analytics/explorer/sidebar/field_insights.tsx +++ b/public/components/event_analytics/explorer/sidebar/field_insights.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useState, useContext, useEffect } from 'react'; import { indexOf, last } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiBasicTable } from '@elastic/eui'; -import { getIndexPatternFromRawQuery } from '../../../../../common/utils/query_utils'; +import { getIndexPatternFromRawQuery } from '../../../common/query_utils'; import { TabContext } from '../../hooks/use_tab_context'; interface IInsightsReq { diff --git a/public/components/event_analytics/explorer/sidebar/sidebar.scss b/public/components/event_analytics/explorer/sidebar/sidebar.scss index cc9557c25..8f96c7764 100644 --- a/public/components/event_analytics/explorer/sidebar/sidebar.scss +++ b/public/components/event_analytics/explorer/sidebar/sidebar.scss @@ -3,33 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -.dscIndexPattern__container { +.explorerIndexPattern__container { display: flex; align-items: center; height: $euiSize * 3; margin-top: -$euiSizeS; } -.dscIndexPattern__triggerButton { +.explorerIndexPattern__triggerButton { @include euiTitle('xs'); line-height: $euiSizeXXL; } -.dscFieldList { +.explorerFieldList { list-style: none; margin-bottom: 0; } -.dscFieldListHeader { +.explorerFieldListHeader { padding: $euiSizeS $euiSizeS 0 $euiSizeS; background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); } -.dscFieldList--popular { +.explorerFieldList--popular { background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); } -.dscFieldChooser__toggle { +.explorerFieldChooser__toggle { color: $euiColorMediumShade; margin-left: $euiSizeS !important; } @@ -38,7 +38,7 @@ * 1. Only visually hide the action, so that it's still accessible to screen readers. * 2. When tabbed to, this element needs to be visible for keyboard accessibility. */ -.dscSidebarItem__action { +.explorerSidebarItem__action { opacity: 0; /* 1 */ transition: none; @@ -54,31 +54,31 @@ } } -.dscFieldSearch { +.explorerFieldSearch { padding: $euiSizeS; } -.dscFieldSearch__toggleButton { +.explorerFieldSearch__toggleButton { width: calc(100% - #{$euiSizeS}); color: $euiColorPrimary; padding-left: $euiSizeXS; margin-left: $euiSizeXS; } -.dscFieldSearch__filterWrapper { +.explorerFieldSearch__filterWrapper { flex-grow: 0; } -.dscFieldSearch__formWrapper { +.explorerFieldSearch__formWrapper { padding: $euiSizeM; } -.dscFieldDetails { +.explorerFieldDetails { color: $euiTextColor; margin-bottom: $euiSizeS; } -.dscSidebarItem__fieldPopoverPanel { +.explorerSidebarItem__fieldPopoverPanel { min-width: 300px; max-width: 600px; max-height: 600px; @@ -124,3 +124,8 @@ .ws__configPanel--right{ overflow-y: unset; } + +.sidebar_content{ + white-space: nowrap; + padding: 0.1px; +} \ No newline at end of file diff --git a/public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap b/public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap index 7255e68b3..de94c5f72 100644 --- a/public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap +++ b/public/components/event_analytics/explorer/timechart_header/__tests__/__snapshots__/timechart_header.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Time chart header component Renders Time chart header component 1`] = ` @@ -157,7 +158,9 @@ exports[`Time chart header component Renders Time chart header component 1`] = ` data-test-subj="discoverIntervalDateRange" onBlur={[Function]} onFocus={[Function]} - /> + > + Aug 28, 2023 @ 20:00:00.406 - Aug 28, 2023 @ 20:00:00.408 + diff --git a/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx b/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx index 2d56d74b9..084142907 100644 --- a/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx +++ b/public/components/event_analytics/explorer/timechart_header/__tests__/timechart_header.test.tsx @@ -9,6 +9,10 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { TimechartHeader } from '../timechart_header'; import { TIME_INTERVAL_OPTIONS } from '../../../../../../common/constants/explorer'; +import { + EXPLORER_START_TIME, + EXPLORER_END_TIME, +} from '../../../../../../test/event_analytics_constants'; describe('Time chart header component', () => { configure({ adapter: new Adapter() }); @@ -18,10 +22,11 @@ describe('Time chart header component', () => { const wrapper = mount( ); diff --git a/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx b/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx index 7b8d10693..f294adc3f 100644 --- a/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx +++ b/public/components/event_analytics/explorer/timechart_header/timechart_header.tsx @@ -7,19 +7,18 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiText, EuiSelect } from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; +import moment from 'moment'; +import datemath from '@elastic/datemath'; +import { + DATE_DISPLAY_FORMAT, + DEFAULT_DATETIME_STRING, +} from '../../../../../common/constants/explorer'; + +function reformatDate(inputDate: string | undefined) { + return moment(datemath.parse(inputDate ?? DEFAULT_DATETIME_STRING)).format(DATE_DISPLAY_FORMAT); +} export interface TimechartHeaderProps { - /** - * Format of date to be displayed - */ - dateFormat?: string; - /** - * Range of dates to be displayed - */ - timeRange?: { - from: string; - to: string; - }; /** * Interval Options */ @@ -32,14 +31,20 @@ export interface TimechartHeaderProps { * selected interval */ stateInterval?: string | undefined; + /** + * current time span being displayed on the count distribution + */ + startTime?: string; + endTime?: string; } export function TimechartHeader({ options, onChangeInterval, stateInterval, + startTime, + endTime, }: TimechartHeaderProps) { - const handleIntervalChange = (e: React.ChangeEvent) => { onChangeInterval(e.target.value); }; @@ -54,7 +59,9 @@ export function TimechartHeader({ })} delay="long" > - + + {reformatDate(startTime) + ' - ' + reformatDate(endTime)} + diff --git a/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index 4d7f5fb29..443c12109 100644 --- a/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -1581,7 +1581,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -1814,7 +1814,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", @@ -2312,7 +2312,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", diff --git a/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap index 03e9b3fb4..b9931d0f3 100644 --- a/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap @@ -44,131 +44,7 @@ exports[`Count distribution component Renders count distribution component with }, } } -> - - -
    - - - +/> `; exports[`Count distribution component Renders empty count distribution component 1`] = ``; diff --git a/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx b/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx index e7ea547df..8e5f10f31 100644 --- a/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx +++ b/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx @@ -6,13 +6,20 @@ import React from 'react'; import { BarOrientation, LONG_CHART_COLOR } from '../../../../../../common/constants/shared'; import { Plt } from '../../../../visualizations/plotly/plot'; +import { fillTimeDataWithEmpty } from '../../../utils/utils'; -export const CountDistribution = ({ countDistribution }: any) => { +export const CountDistribution = ({ + countDistribution, + selectedInterval, + startTime, + endTime, +}: any) => { if ( !countDistribution || !countDistribution.data || !countDistribution.metadata || - !countDistribution.metadata.fields + !countDistribution.metadata.fields || + !selectedInterval ) return null; @@ -31,9 +38,31 @@ export const CountDistribution = ({ countDistribution }: any) => { }, ]; + // fill the final data with the exact right amount of empty buckets + function fillWithEmpty(processedData: any) { + // original x and y fields + const xVals = processedData[0].x; + const yVals = processedData[0].y; + + const { buckets, values } = fillTimeDataWithEmpty( + xVals, + yVals, + selectedInterval.replace(/^auto_/, ''), + startTime, + endTime + ); + + // replace old x and y values with new + processedData[0].x = buckets; + processedData[0].y = values; + + // // at the end, return the new object + return processedData; + } + return ( - +
    @@ -1279,18 +1278,17 @@ exports[`Saved query table component Renders saved query table 1`] = ` - + diff --git a/public/components/event_analytics/home/home.tsx b/public/components/event_analytics/home/home.tsx index 112a11324..b75523159 100644 --- a/public/components/event_analytics/home/home.tsx +++ b/public/components/event_analytics/home/home.tsx @@ -23,7 +23,6 @@ import { EuiSpacer, EuiText, EuiTitle, - htmlIdGenerator, } from '@elastic/eui'; import React, { ReactElement, useEffect, useRef, useState } from 'react'; import { connect } from 'react-redux'; @@ -34,10 +33,8 @@ import { EVENT_ANALYTICS_DOCUMENTATION_URL, NEW_TAB, RAW_QUERY, - REDIRECT_TAB, SELECTED_DATE_RANGE, TAB_CREATED_TYPE, - TAB_ID_TXT_PFX, } from '../../../../common/constants/explorer'; import { EVENT_ANALYTICS, @@ -56,11 +53,9 @@ import { getSampleDataModal } from '../../common/helpers/add_sample_modal'; import { DeleteModal } from '../../common/helpers/delete_modal'; import { onItemSelect, parseGetSuggestions } from '../../common/search/autocomplete_logic'; import { Search } from '../../common/search/search'; -import { init as initFields } from '../redux/slices/field_slice'; -import { init as initPatterns } from '../redux/slices/patterns_slice'; -import { init as initQueryResult, selectQueryResult } from '../redux/slices/query_result_slice'; -import { changeQuery, init as initQuery, selectQueries } from '../redux/slices/query_slice'; -import { addTab, selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; +import { selectQueryResult } from '../redux/slices/query_result_slice'; +import { changeQuery, selectQueries } from '../redux/slices/query_slice'; +import { selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; import { SavedQueryTable } from './saved_objects_table'; interface IHomeProps { @@ -84,7 +79,6 @@ const EventAnalyticsHome = (props: IHomeProps) => { const { pplService, dslService, - savedObjects, setToast, getExistingEmptyTab, http, @@ -144,22 +138,6 @@ const EventAnalyticsHome = (props: IHomeProps) => { }); }; - const addNewTab = async () => { - // get a new tabId - const tabId = htmlIdGenerator(TAB_ID_TXT_PFX)(); - - // create a new tab - await batch(() => { - dispatch(initQuery({ tabId })); - dispatch(initQueryResult({ tabId })); - dispatch(initFields({ tabId })); - dispatch(addTab({ tabId })); - dispatch(initPatterns({ tabId })); - }); - - return tabId; - }; - useEffect(() => { fetchHistories(); }, []); @@ -186,10 +164,9 @@ const EventAnalyticsHome = (props: IHomeProps) => { queries, explorerData, }); - const newTabId = emptyTabId ? emptyTabId : await addNewTab(); // update this new tab with data - await dispatchInitialData(newTabId); + await dispatchInitialData(emptyTabId); // redirect to explorer history.push('/explorer'); @@ -199,28 +176,6 @@ const EventAnalyticsHome = (props: IHomeProps) => { const handleTimePickerChange = async (timeRange: string[]) => setSelectedDateRange(timeRange); - const handleHistoryClick = async (objectId: string) => { - const emptyTabId = getExistingEmptyTab({ - tabIds: tabsState.queryTabIds, - queries, - explorerData, - }); - const newTabId = emptyTabId ? emptyTabId : await addNewTab(); - batch(() => { - dispatch( - changeQuery({ - tabId: newTabId, - query: { - [TAB_CREATED_TYPE]: REDIRECT_TAB, - }, - }) - ); - dispatch(setSelectedQueryTab({ tabId: newTabId })); - }); - // redirect to explorer - history.push(`/explorer/${objectId}`); - }; - const addSampledata = async () => { setModalLayout( getSampleDataModal(closeModal, async () => { @@ -404,7 +359,6 @@ const EventAnalyticsHome = (props: IHomeProps) => { {savedHistories.length > 0 ? ( { return ( { - handleHistoryClick(item.objectId); - }} + href={`${LOG_EXPLORER_BASE_PATH}${item.objectId}`} data-test-subj="eventHome__savedQueryTableName" > {item.name} diff --git a/public/components/event_analytics/hooks/use_fetch_patterns.ts b/public/components/event_analytics/hooks/use_fetch_patterns.ts index dc26b57b3..b50aa2467 100644 --- a/public/components/event_analytics/hooks/use_fetch_patterns.ts +++ b/public/components/event_analytics/hooks/use_fetch_patterns.ts @@ -15,7 +15,7 @@ import { SELECTED_PATTERN_FIELD, SELECTED_TIMESTAMP, } from '../../../../common/constants/explorer'; -import { buildPatternsQuery } from '../../../../common/utils/query_utils'; +import { buildPatternsQuery } from '../../common/query_utils'; import { IPPLEventsDataSource } from '../../../../server/common/types'; import { reset as resetPatterns, setPatterns } from '../redux/slices/patterns_slice'; import { changeQuery, selectQueries } from '../redux/slices/query_slice'; @@ -97,7 +97,7 @@ export const useFetchPatterns = ({ pplService, requestParams }: IFetchPatternsPa fetchEvents({ query: anomaliesQuery }, 'jdbc', (res) => res), ]) .then((res) => { - const [statsResp, anomaliesResp] = res as PromiseSettledResult[]; + const [statsResp, anomaliesResp] = res as Array>; if (statsResp.status === 'rejected') { throw statsResp.reason; } diff --git a/public/components/event_analytics/index.tsx b/public/components/event_analytics/index.tsx index e0c89b243..959a96b73 100644 --- a/public/components/event_analytics/index.tsx +++ b/public/components/event_analytics/index.tsx @@ -6,10 +6,8 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { EmptyTabParams, EventAnalyticsProps } from 'common/types/explorer'; -import { isEmpty } from 'lodash'; import React, { createContext, ReactChild, useState } from 'react'; -import { HashRouter, Route, RouteComponentProps, Switch, useHistory } from 'react-router-dom'; -import { RAW_QUERY } from '../../../common/constants/explorer'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import '../../variables.scss'; import { LogExplorer } from './explorer/log_explorer'; import { Home as EventExplorerHome } from './home/home'; @@ -44,17 +42,7 @@ export const EventAnalytics = ({ setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); }; - const getExistingEmptyTab = ({ tabIds, queries, explorerData }: EmptyTabParams) => { - let emptyTabId = ''; - for (let i = 0; i < tabIds!.length; i++) { - const tid = tabIds![i]; - if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { - emptyTabId = tid; - break; - } - } - return emptyTabId; - }; + const getExistingEmptyTab = ({ tabIds }: EmptyTabParams) => tabIds[0]; return ( <> diff --git a/public/components/event_analytics/utils/__tests__/utils.test.tsx b/public/components/event_analytics/utils/__tests__/utils.test.tsx index 12d9a9cda..f6b5138d2 100644 --- a/public/components/event_analytics/utils/__tests__/utils.test.tsx +++ b/public/components/event_analytics/utils/__tests__/utils.test.tsx @@ -13,7 +13,10 @@ import { isValidTraceId, rangeNumDocs, getHeaders, + fillTimeDataWithEmpty, + redoQuery, } from '../utils'; +import { EXPLORER_DATA_GRID_QUERY } from '../../../../../test/event_analytics_constants'; describe('Utils event analytics helper functions', () => { configure({ adapter: new Adapter() }); @@ -49,35 +52,93 @@ describe('Utils event analytics helper functions', () => { expect(rangeNumDocs(2000)).toBe(2000); }); - it('validates getHeaders function', () => { + it('validates fillTimeDataWithEmpty function', () => { expect( - getHeaders( + fillTimeDataWithEmpty( + ['2023-07-01 00:00:00', '2023-08-01 00:00:00', '2023-09-01 00:00:00'], + [54, 802, 292], + 'M', + '2023-01-01T08:00:00.000Z', + '2023-09-12T21:36:31.389Z' + ) + ).toEqual({ + buckets: [ + '2023-01-01 00:00:00', + '2023-02-01 00:00:00', + '2023-03-01 00:00:00', + '2023-04-01 00:00:00', + '2023-05-01 00:00:00', + '2023-06-01 00:00:00', + '2023-07-01 00:00:00', + '2023-08-01 00:00:00', + '2023-09-01 00:00:00', + ], + values: [0, 0, 0, 0, 0, 0, 54, 802, 292], + }); + expect( + fillTimeDataWithEmpty( [ + '2023-09-11 07:00:00', + '2023-09-11 09:00:00', + '2023-09-11 10:00:00', + '2023-09-11 11:00:00', + '2023-09-11 12:00:00', + '2023-09-11 13:00:00', + '2023-09-11 14:00:00', + '2023-09-11 15:00:00', + ], + [1, 1, 5, 4, 2, 3, 3, 1], + 'h', + '2023-09-11T00:00:00.000', + '2023-09-11T17:00:00.000' + ) + ).toEqual({ + buckets: [ + '2023-09-11 00:00:00', + '2023-09-11 01:00:00', + '2023-09-11 02:00:00', + '2023-09-11 03:00:00', + '2023-09-11 04:00:00', + '2023-09-11 05:00:00', + '2023-09-11 06:00:00', + '2023-09-11 07:00:00', + '2023-09-11 08:00:00', + '2023-09-11 09:00:00', + '2023-09-11 10:00:00', + '2023-09-11 11:00:00', + '2023-09-11 12:00:00', + '2023-09-11 13:00:00', + '2023-09-11 14:00:00', + '2023-09-11 15:00:00', + '2023-09-11 16:00:00', + '2023-09-11 17:00:00', + ], + values: [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 5, 4, 2, 3, 3, 1, 0, 0], + }); + }); + + it('validates redoQuery function', () => { + const getEvents = jest.fn(); + redoQuery( + '2023-01-01 00:00:00', + '2023-09-28 23:19:10', + "source = opensearch_dashboards_sample_data_logs | where match(request,'filebeat')", + 'timestamp', + { + current: [ { - name: 'host', - type: 'text', - }, - { - name: 'ip_count', - type: 'integer', - }, - { - name: 'per_ip_bytes', - type: 'long', - }, - { - name: 'resp_code', - type: 'text', - }, - { - name: 'sum_bytes', - type: 'long', + id: 'timestamp', + direction: 'asc', }, ], - ['', 'Time', '_source'], - undefined - ) - ).toBeTruthy(); - expect(getHeaders([], ['', 'Time', '_source'], undefined)).toBeTruthy(); + }, + { + current: [0, 100], + }, + getEvents + ); + const expectedFinalQuery = + "source=opensearch_dashboards_sample_data_logs | where timestamp >= '2023-01-01 00:00:00' and timestamp <= '2023-09-28 23:19:10' | where match(request,'filebeat') | sort + timestamp | head 100 from 0"; + expect(getEvents).toBeCalledWith(expectedFinalQuery); }); }); diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index cc786f0d8..3266b51f2 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -1,13 +1,15 @@ -/* eslint-disable no-bitwise */ /* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +/* eslint-disable no-bitwise */ + import { uniqueId, isEmpty } from 'lodash'; import moment from 'moment'; -import React from 'react'; -import { EuiText } from '@elastic/eui'; +import React, { MutableRefObject } from 'react'; +import { EuiDataGridSorting, EuiText } from '@elastic/eui'; +import datemath from '@elastic/datemath'; import { HttpStart } from '../../../../../../src/core/public'; import { CUSTOM_LABEL, @@ -15,14 +17,21 @@ import { GROUPBY, AGGREGATIONS, BREAKDOWNS, + DATE_PICKER_FORMAT, } from '../../../../common/constants/explorer'; -import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; +import { + PPL_DATE_FORMAT, + PPL_INDEX_INSERT_POINT_REGEX, + PPL_INDEX_REGEX, + PPL_NEWLINE_REGEX, +} from '../../../../common/constants/shared'; import { ConfigListEntry, GetTooltipHoverInfoType, IExplorerFields, IField, IQuery, + MOMENT_UNIT_OF_TIME, } from '../../../../common/types/explorer'; import PPLService from '../../../services/requests/ppl'; import { DocViewRow, IDocType } from '../explorer/events_views'; @@ -34,123 +43,6 @@ import { statsChunk, } from '../../../../common/query_manager/ast/types'; -// Create Individual table rows for events datagrid and flyouts -export const getTrs = ( - http: HttpStart, - explorerFields: IField[], - limit: number, - setLimit: React.Dispatch>, - pageSize: number, - timeStampField: any, - explorerFieldsFull: IExplorerFields, - pplService: PPLService, - rawQuery: string, - rowRefs: Array< - React.RefObject<{ - closeAllFlyouts(openDocId: string): void; - }> - >, - setRowRefs: React.Dispatch< - React.SetStateAction< - Array< - React.RefObject<{ - closeAllFlyouts(openDocId: string): void; - }> - > - > - >, - onFlyoutOpen: (docId: string) => void, - docs: any[] = [], - prevTrs: any[] = [] -) => { - if (prevTrs.length >= docs.length) return prevTrs; - - // reset limit if no previous table rows - if (prevTrs.length === 0 && limit !== pageSize) setLimit(pageSize); - const trs = prevTrs.slice(); - - const upperLimit = Math.min(trs.length === 0 ? pageSize : limit, docs.length); - const tempRefs = rowRefs; - for (let i = trs.length; i < upperLimit; i++) { - const docId = uniqueId('doc_view'); - const tempRowRef = React.createRef<{ - closeAllFlyouts(openDocId: string): void; - }>(); - tempRefs.push(tempRowRef); - trs.push( - - ); - } - setRowRefs(tempRefs); - return trs; -}; - -// Create table headers for events datagrid and flyouts -export const getHeaders = (fields: any, defaultCols: string[], isFlyout?: boolean) => { - let tableHeadContent = null; - if (!fields || fields.length === 0) { - tableHeadContent = ( - <> - {defaultCols.map((colName: string) => { - return {colName}; - })} - - ); - } else { - tableHeadContent = fields.map((selField: any) => { - return {selField.name}; - }); - - if (!isFlyout) { - tableHeadContent.unshift(); - } - } - - return {tableHeadContent}; -}; - -// Populate Events datagrid and flyouts -export const populateDataGrid = ( - explorerFields: IExplorerFields, - header1: JSX.Element, - body1: JSX.Element, - header2: JSX.Element, - body2: JSX.Element -) => { - return ( - <> -
    - {explorerFields?.queriedFields && explorerFields.queriedFields.length > 0 && ( - - {header1} - {body1} -
    - )} - {explorerFields?.queriedFields && - explorerFields?.queriedFields?.length > 0 && - explorerFields.selectedFields?.length === 0 ? null : ( - - {header2} - {body2} -
    - )} -
    - - ); -}; - /* Builds Final Query for the surrounding events * -> Final Query is as follows: * -> finalQuery = indexPartOfQuery + timeQueryFilter + filterPartOfQuery + sortFilter @@ -391,13 +283,15 @@ export const getDefaultVisConfig = (statsToken: statsChunk) => { // const seriesToken = statsToken.aggregations && statsToken.aggregations[0]; const span = getSpanValue(groupByToken); return { - [AGGREGATIONS]: statsToken.aggregations.map((agg) => ({ - label: agg.function?.value_expression, - name: agg.function?.value_expression, - aggregation: agg.function?.name, - [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof StatsAggregationChunk], - })), - [GROUPBY]: groupByToken?.group_fields?.map((agg) => ({ + [AGGREGATIONS]: statsToken.aggregations.map( + (agg: { [x: string]: any; function: { value_expression: any; name: any } }) => ({ + label: agg.function?.value_expression, + name: agg.function?.value_expression, + aggregation: agg.function?.name, + [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof StatsAggregationChunk], + }) + ), + [GROUPBY]: groupByToken?.group_fields?.map((agg: { [x: string]: any; name: any }) => ({ label: agg.name ?? '', name: agg.name ?? '', [CUSTOM_LABEL]: agg[CUSTOM_LABEL as keyof GroupField] ?? '', @@ -459,3 +353,88 @@ export const getContentTabTitle = (tabID: string, tabTitle: string) => { ); }; + +/** + * Used to fill in missing empty data where x is an array of time values and there are only x + * values when y is non-zero. + * @param xVals all x values being used + * @param yVals all y values being used + * @param intervalPeriod Moment unitOfTime used to dictate how long each interval is + * @param startTime starting time of x values + * @param endTime ending time of x values + * @returns an object with buckets and values where the buckets are all of the new x values and + * values are the corresponding values which include y values that are 0 for empty data + */ +export const fillTimeDataWithEmpty = ( + xVals: string[], + yVals: number[], + intervalPeriod: MOMENT_UNIT_OF_TIME, + startTime: string, + endTime: string +): { buckets: string[]; values: number[] } => { + // parses out datetime for start and end, then reformats + const startDate = datemath + .parse(startTime) + ?.startOf(intervalPeriod === 'w' ? 'isoWeek' : intervalPeriod); + const endDate = datemath + .parse(endTime) + ?.startOf(intervalPeriod === 'w' ? 'isoWeek' : intervalPeriod); + + // find the number of buckets + // below essentially does ((end - start) / interval_period) + 1 + const numBuckets = endDate.diff(startDate, intervalPeriod) + 1; + + // populate buckets as x values in the graph + const buckets = [startDate.format(DATE_PICKER_FORMAT)]; + const currentDate = startDate; + for (let i = 1; i < numBuckets; i++) { + const nextBucket = currentDate.add(1, intervalPeriod); + buckets.push(nextBucket.format(DATE_PICKER_FORMAT)); + } + + // create y values, use old y values if they exist + const values: number[] = []; + buckets.forEach((bucket) => { + const bucketIndex = xVals.findIndex((x: string) => x === bucket); + if (bucketIndex !== -1) { + values.push(yVals[bucketIndex]); + } else { + values.push(0); + } + }); + + return { buckets, values }; +}; + +export const redoQuery = ( + startTime: string, + endTime: string, + rawQuery: string, + timeStampField: string, + sortingFields: MutableRefObject, + pageFields: MutableRefObject, + getEvents: any +) => { + let finalQuery = ''; + + const start = datemath.parse(startTime)?.utc().format(DATE_PICKER_FORMAT); + const end = datemath.parse(endTime, { roundUp: true })?.utc().format(DATE_PICKER_FORMAT); + const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); + + finalQuery = `${tokens![1]}=${ + tokens![2] + } | where ${timeStampField} >= '${start}' and ${timeStampField} <= '${end}'`; + + finalQuery += tokens![3]; + + for (let i = 0; i < sortingFields.current.length; i++) { + const field = sortingFields.current[i]; + const dir = field.direction === 'asc' ? '+' : '-'; + finalQuery = finalQuery + ` | sort ${dir} ${field.id}`; + } + + finalQuery = + finalQuery + + ` | head ${pageFields.current[1]} from ${pageFields.current[0] * pageFields.current[1]}`; + getEvents(finalQuery); +}; diff --git a/public/components/integrations/components/__tests__/__snapshots__/added_integration_flyout.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/added_integration_flyout.test.tsx.snap index 9ae4ce75e..3ba54c92b 100644 --- a/public/components/integrations/components/__tests__/__snapshots__/added_integration_flyout.test.tsx.snap +++ b/public/components/integrations/components/__tests__/__snapshots__/added_integration_flyout.test.tsx.snap @@ -2,7 +2,14 @@ exports[`Add Integration Flyout Test Renders add integration flyout with dummy integration name 1`] = ` diff --git a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap new file mode 100644 index 000000000..fcfd3d48f --- /dev/null +++ b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap @@ -0,0 +1,1706 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Integration Setup Page Renders integration setup page as expected 1`] = ` + + +
    + +
    + + +
    + +
    + + +
    + +

    + Set Up Integration +

    +
    + +
    + + +

    + Integration Details +

    +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + +
    + + +

    + Integration Connection +

    +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + + +
    + + + + + +
    +
    +
    +
    +
    +
    + +
    + Select a data source to connect to. +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    + +
    + + +
    +
    +
    + + + + +
    + +
    +
    + +
    + +
    + + + + + + + + + + + +
    +
    +
    +
    + + +
    + + +
    + Select an index to pull the data from. +
    +
    +
    +
    + +
    + + +
    + +
    + + + + + + +
    +

    + Page level controls +

    +
    +
    + +
    +
    + +
    +
    +
    +

    + There is a new region landmark with page level controls at the end of the document. +

    +
    + } + > + +
    + +

    + Page level controls +

    +
    + +
    + +
    + + + + + +
    +
    + +
    + + + + + +
    +
    +
    +
    +
    +
    + +

    + + There is a new region landmark with page level controls at the end of the document. + +

    +
    + + + + +
    + +
    + + +`; + +exports[`Integration Setup Page Renders the form as expected 1`] = ` + + +
    + +

    + Set Up Integration +

    +
    + +
    + + +

    + Integration Details +

    +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + +
    + + +

    + Integration Connection +

    +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + + +
    + + + + + +
    +
    +
    +
    +
    +
    + +
    + Select a data source to connect to. +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    + +
    + + +
    +
    +
    + + + ss4o_logs-nginx-test + + + +
    + +
    +
    + +
    + +
    + + + + + + + + + + + +
    +
    +
    +
    + + +
    + + +
    + Select an index to pull the data from. +
    +
    +
    +
    + +
    + + +`; diff --git a/public/components/integrations/components/__tests__/added_integration_flyout.test.tsx b/public/components/integrations/components/__tests__/added_integration_flyout.test.tsx index 7f5280652..9099e59a1 100644 --- a/public/components/integrations/components/__tests__/added_integration_flyout.test.tsx +++ b/public/components/integrations/components/__tests__/added_integration_flyout.test.tsx @@ -8,13 +8,25 @@ import Adapter from 'enzyme-adapter-react-16'; import { waitFor } from '@testing-library/react'; import { AddIntegrationFlyout } from '../add_integration_flyout'; import React from 'react'; +import { HttpSetup } from '../../../../../../../src/core/public'; describe('Add Integration Flyout Test', () => { configure({ adapter: new Adapter() }); it('Renders add integration flyout with dummy integration name', async () => { const wrapper = mount( - + ) as HttpSetup + } + /> ); await waitFor(() => { diff --git a/public/components/integrations/components/__tests__/create_integration_helpers.test.ts b/public/components/integrations/components/__tests__/create_integration_helpers.test.ts new file mode 100644 index 000000000..71ccc99bf --- /dev/null +++ b/public/components/integrations/components/__tests__/create_integration_helpers.test.ts @@ -0,0 +1,333 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + checkDataSourceName, + doTypeValidation, + doNestedPropertyValidation, + doPropertyValidation, + fetchDataSourceMappings, + fetchIntegrationMappings, + doExistingDataSourceValidation, +} from '../create_integration_helpers'; +import * as create_integration_helpers from '../create_integration_helpers'; +import { HttpSetup } from '../../../../../../../src/core/public'; + +describe('doTypeValidation', () => { + it('should return true if required type is not specified', () => { + const toCheck = { type: 'string' }; + const required = {}; + + const result = doTypeValidation(toCheck, required); + + expect(result.ok).toBe(true); + }); + + it('should return true if types match', () => { + const toCheck = { type: 'string' }; + const required = { type: 'string' }; + + const result = doTypeValidation(toCheck, required); + + expect(result.ok).toBe(true); + }); + + it('should return true if object has properties', () => { + const toCheck = { properties: { prop1: { type: 'string' } } }; + const required = { type: 'object' }; + + const result = doTypeValidation(toCheck, required); + + expect(result.ok).toBe(true); + }); + + it('should return false if types do not match', () => { + const toCheck = { type: 'string' }; + const required = { type: 'number' }; + + const result = doTypeValidation(toCheck, required); + + expect(result.ok).toBe(false); + }); +}); + +describe('doNestedPropertyValidation', () => { + it('should return true if type validation passes and no properties are required', () => { + const toCheck = { type: 'string' }; + const required = { type: 'string' }; + + const result = doNestedPropertyValidation(toCheck, required); + + expect(result.ok).toBe(true); + }); + + it('should return false if type validation fails', () => { + const toCheck = { type: 'string' }; + const required = { type: 'number' }; + + const result = doNestedPropertyValidation(toCheck, required); + + expect(result.ok).toBe(false); + }); + + it('should return false if a required property is missing', () => { + const toCheck = { type: 'object', properties: { prop1: { type: 'string' } } }; + const required = { + type: 'object', + properties: { prop1: { type: 'string' }, prop2: { type: 'number' } }, + }; + + const result = doNestedPropertyValidation(toCheck, required); + + expect(result.ok).toBe(false); + }); + + it('should return true if all required properties pass validation', () => { + const toCheck = { + type: 'object', + properties: { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }, + }; + const required = { + type: 'object', + properties: { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }, + }; + + const result = doNestedPropertyValidation(toCheck, required); + + expect(result.ok).toBe(true); + }); +}); + +describe('doPropertyValidation', () => { + it('should return true if all properties pass validation', () => { + const rootType = 'root'; + const dataSourceProps = { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }; + const requiredMappings = { + root: { + template: { + mappings: { + properties: { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }, + }, + }, + }, + }; + + const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); + + expect(result.ok).toBe(true); + }); + + it('should return false if a property fails validation', () => { + const rootType = 'root'; + const dataSourceProps = { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }; + const requiredMappings = { + root: { + template: { + mappings: { + properties: { + prop1: { type: 'string' }, + prop2: { type: 'boolean' }, + }, + }, + }, + }, + }; + + const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); + + expect(result.ok).toBe(false); + }); + + it('should return false if a required nested property is missing', () => { + const rootType = 'root'; + const dataSourceProps = { + prop1: { type: 'string' }, + }; + const requiredMappings = { + root: { + template: { + mappings: { + properties: { + prop1: { type: 'string' }, + prop2: { type: 'number' }, + }, + }, + }, + }, + }; + + const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); + + expect(result.ok).toBe(false); + }); +}); + +describe('checkDataSourceName', () => { + it('Filters out invalid index names', () => { + const result = checkDataSourceName('ss4o_logs-no-exclams!', 'logs'); + + expect(result.ok).toBe(false); + }); + + it('Filters out incorrectly typed indices', () => { + const result = checkDataSourceName('ss4o_metrics-test-test', 'logs'); + + expect(result.ok).toBe(false); + }); + + it('Accepts correct indices', () => { + const result = checkDataSourceName('ss4o_logs-test-test', 'logs'); + + expect(result.ok).toBe(true); + }); +}); + +describe('fetchDataSourceMappings', () => { + it('Retrieves mappings', async () => { + const mockHttp = { + post: jest.fn().mockResolvedValue({ + source1: { mappings: { properties: { test: true } } }, + source2: { mappings: { properties: { test: true } } }, + }), + } as Partial; + + const result = fetchDataSourceMappings('sample', mockHttp as HttpSetup); + + await expect(result).resolves.toMatchObject({ + source1: { properties: { test: true } }, + source2: { properties: { test: true } }, + }); + }); + + it('Catches errors', async () => { + const mockHttp = { + post: jest.fn().mockRejectedValue(new Error('Mock error')), + } as Partial; + + const result = fetchDataSourceMappings('sample', mockHttp as HttpSetup); + + await expect(result).resolves.toBeNull(); + }); +}); + +describe('fetchIntegrationMappings', () => { + it('Returns schema mappings', async () => { + const mockHttp = { + get: jest.fn().mockResolvedValue({ data: { mappings: { test: true } }, statusCode: 200 }), + } as Partial; + + const result = fetchIntegrationMappings('target', mockHttp as HttpSetup); + + await expect(result).resolves.toStrictEqual({ test: true }); + }); + + it('Returns null if response fails', async () => { + const mockHttp = { + get: jest.fn().mockResolvedValue({ statusCode: 404 }), + } as Partial; + + const result = fetchIntegrationMappings('target', mockHttp as HttpSetup); + + await expect(result).resolves.toBeNull(); + }); + + it('Catches request error', async () => { + const mockHttp = { + get: jest.fn().mockRejectedValue(new Error('mock error')), + } as Partial; + + const result = fetchIntegrationMappings('target', mockHttp as HttpSetup); + + await expect(result).resolves.toBeNull(); + }); +}); + +describe('doExistingDataSourceValidation', () => { + it('Catches and returns checkDataSourceName errors', async () => { + const mockHttp = {} as Partial; + jest + .spyOn(create_integration_helpers, 'checkDataSourceName') + .mockReturnValue({ ok: false, errors: ['mock'] }); + + const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); + + await expect(result).resolves.toHaveProperty('ok', false); + }); + + it('Catches data stream fetch errors', async () => { + const mockHttp = {} as Partial; + jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); + jest.spyOn(create_integration_helpers, 'fetchDataSourceMappings').mockResolvedValue(null); + jest + .spyOn(create_integration_helpers, 'fetchIntegrationMappings') + .mockResolvedValue({ test: { template: { mappings: {} } } }); + + const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); + + await expect(result).resolves.toHaveProperty('ok', false); + }); + + it('Catches integration fetch errors', async () => { + const mockHttp = {} as Partial; + jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); + jest + .spyOn(create_integration_helpers, 'fetchDataSourceMappings') + .mockResolvedValue({ test: { properties: {} } }); + jest.spyOn(create_integration_helpers, 'fetchIntegrationMappings').mockResolvedValue(null); + + const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); + + await expect(result).resolves.toHaveProperty('ok', false); + }); + + it('Catches type validation issues', async () => { + const mockHttp = {} as Partial; + jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); + jest + .spyOn(create_integration_helpers, 'fetchDataSourceMappings') + .mockResolvedValue({ test: { properties: {} } }); + jest + .spyOn(create_integration_helpers, 'fetchIntegrationMappings') + .mockResolvedValue({ test: { template: { mappings: {} } } }); + jest + .spyOn(create_integration_helpers, 'doPropertyValidation') + .mockReturnValue({ ok: false, errors: ['mock'] }); + + const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); + + await expect(result).resolves.toHaveProperty('ok', false); + }); + + it('Returns no errors if everything passes', async () => { + const mockHttp = {} as Partial; + jest.spyOn(create_integration_helpers, 'checkDataSourceName').mockReturnValue({ ok: true }); + jest + .spyOn(create_integration_helpers, 'fetchDataSourceMappings') + .mockResolvedValue({ test: { properties: {} } }); + jest + .spyOn(create_integration_helpers, 'fetchIntegrationMappings') + .mockResolvedValue({ test: { template: { mappings: {} } } }); + jest.spyOn(create_integration_helpers, 'doPropertyValidation').mockReturnValue({ ok: true }); + + const result = doExistingDataSourceValidation('target', 'name', 'type', mockHttp as HttpSetup); + + await expect(result).resolves.toHaveProperty('ok', true); + }); +}); diff --git a/public/components/integrations/components/__tests__/mapping_validation.test.ts b/public/components/integrations/components/__tests__/mapping_validation.test.ts deleted file mode 100644 index 4a02058cf..000000000 --- a/public/components/integrations/components/__tests__/mapping_validation.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - doTypeValidation, - doNestedPropertyValidation, - doPropertyValidation, -} from '../add_integration_flyout'; - -describe('Validation', () => { - describe('doTypeValidation', () => { - it('should return true if required type is not specified', () => { - const toCheck = { type: 'string' }; - const required = {}; - - const result = doTypeValidation(toCheck, required); - - expect(result).toBe(true); - }); - - it('should return true if types match', () => { - const toCheck = { type: 'string' }; - const required = { type: 'string' }; - - const result = doTypeValidation(toCheck, required); - - expect(result).toBe(true); - }); - - it('should return true if object has properties', () => { - const toCheck = { properties: { prop1: { type: 'string' } } }; - const required = { type: 'object' }; - - const result = doTypeValidation(toCheck, required); - - expect(result).toBe(true); - }); - - it('should return false if types do not match', () => { - const toCheck = { type: 'string' }; - const required = { type: 'number' }; - - const result = doTypeValidation(toCheck, required); - - expect(result).toBe(false); - }); - }); - - describe('doNestedPropertyValidation', () => { - it('should return true if type validation passes and no properties are required', () => { - const toCheck = { type: 'string' }; - const required = { type: 'string' }; - - const result = doNestedPropertyValidation(toCheck, required); - - expect(result).toBe(true); - }); - - it('should return false if type validation fails', () => { - const toCheck = { type: 'string' }; - const required = { type: 'number' }; - - const result = doNestedPropertyValidation(toCheck, required); - - expect(result).toBe(false); - }); - - it('should return false if a required property is missing', () => { - const toCheck = { type: 'object', properties: { prop1: { type: 'string' } } }; - const required = { - type: 'object', - properties: { prop1: { type: 'string' }, prop2: { type: 'number' } }, - }; - - const result = doNestedPropertyValidation(toCheck, required); - - expect(result).toBe(false); - }); - - it('should return true if all required properties pass validation', () => { - const toCheck = { - type: 'object', - properties: { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }, - }; - const required = { - type: 'object', - properties: { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }, - }; - - const result = doNestedPropertyValidation(toCheck, required); - - expect(result).toBe(true); - }); - }); - - describe('doPropertyValidation', () => { - it('should return true if all properties pass validation', () => { - const rootType = 'root'; - const dataSourceProps = { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }; - const requiredMappings = { - root: { - template: { - mappings: { - properties: { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }, - }, - }, - }, - }; - - const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); - - expect(result).toBe(true); - }); - - it('should return false if a property fails validation', () => { - const rootType = 'root'; - const dataSourceProps = { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }; - const requiredMappings = { - root: { - template: { - mappings: { - properties: { - prop1: { type: 'string' }, - prop2: { type: 'boolean' }, - }, - }, - }, - }, - }; - - const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); - - expect(result).toBe(false); - }); - - it('should return false if a required nested property is missing', () => { - const rootType = 'root'; - const dataSourceProps = { - prop1: { type: 'string' }, - }; - const requiredMappings = { - root: { - template: { - mappings: { - properties: { - prop1: { type: 'string' }, - prop2: { type: 'number' }, - }, - }, - }, - }, - }; - - const result = doPropertyValidation(rootType, dataSourceProps as any, requiredMappings); - - expect(result).toBe(false); - }); - }); -}); diff --git a/public/components/integrations/components/__tests__/setup_integration.test.tsx b/public/components/integrations/components/__tests__/setup_integration.test.tsx new file mode 100644 index 000000000..7e0a90f2c --- /dev/null +++ b/public/components/integrations/components/__tests__/setup_integration.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import React from 'react'; +import { waitFor } from '@testing-library/react'; +import { SetupIntegrationPage, SetupIntegrationForm } from '../setup_integration'; +import { + TEST_INTEGRATION_CONFIG, + TEST_INTEGRATION_SETUP_INPUTS, +} from '../../../../../test/constants'; + +describe('Integration Setup Page', () => { + configure({ adapter: new Adapter() }); + + it('Renders integration setup page as expected', async () => { + const wrapper = mount(); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + it('Renders the form as expected', async () => { + const wrapper = mount( + {}} + integration={TEST_INTEGRATION_CONFIG} + /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/public/components/integrations/components/add_integration_flyout.tsx b/public/components/integrations/components/add_integration_flyout.tsx index 29cc8921e..a06d4bdda 100644 --- a/public/components/integrations/components/add_integration_flyout.tsx +++ b/public/components/integrations/components/add_integration_flyout.tsx @@ -22,6 +22,7 @@ import { import React, { useState } from 'react'; import { HttpStart } from '../../../../../../src/core/public'; import { useToast } from '../../../../public/components/common/toast'; +import { doExistingDataSourceValidation } from './create_integration_helpers'; interface IntegrationFlyoutProps { onClose: () => void; @@ -31,65 +32,6 @@ interface IntegrationFlyoutProps { http: HttpStart; } -export const doTypeValidation = (toCheck: any, required: any): boolean => { - if (!required.type) { - return true; - } - if (required.type === 'object') { - return Boolean(toCheck.properties); - } - return required.type === toCheck.type; -}; - -export const doNestedPropertyValidation = ( - toCheck: { type?: string; properties?: any }, - required: { type?: string; properties?: any } -): boolean => { - if (!doTypeValidation(toCheck, required)) { - return false; - } - if (required.properties) { - return Object.keys(required.properties).every((property: string) => { - if (!toCheck.properties[property]) { - return false; - } - return doNestedPropertyValidation( - toCheck.properties[property], - required.properties[property] - ); - }); - } - return true; -}; - -export const doPropertyValidation = ( - rootType: string, - dataSourceProps: { [key: string]: { properties?: any } }, - requiredMappings: { [key: string]: { template: { mappings: { properties?: any } } } } -): boolean => { - // Check root object type (without dependencies) - for (const [key, value] of Object.entries( - requiredMappings[rootType].template.mappings.properties - )) { - if (!dataSourceProps[key] || !doNestedPropertyValidation(dataSourceProps[key], value as any)) { - return false; - } - } - // Check nested dependencies - for (const [key, value] of Object.entries(requiredMappings)) { - if (key === rootType) { - continue; - } - if ( - !dataSourceProps[key] || - !doNestedPropertyValidation(dataSourceProps[key], value.template.mappings.properties) - ) { - return false; - } - } - return true; -}; - export function AddIntegrationFlyout(props: IntegrationFlyoutProps) { const { onClose, onCreate, integrationName, integrationType, http } = props; @@ -110,93 +52,6 @@ export function AddIntegrationFlyout(props: IntegrationFlyoutProps) { setName(e.target.value); }; - // Returns true if the data stream is a legal name. - // Appends any additional validation errors to the provided errors array. - const checkDataSourceName = (targetDataSource: string, validationErrors: string[]): boolean => { - if (!Boolean(targetDataSource.match(/^[a-z\d\.][a-z\d\._\-\*]*$/))) { - validationErrors.push('This is not a valid index name.'); - setErrors(validationErrors); - return false; - } - const nameValidity: boolean = Boolean( - targetDataSource.match(new RegExp(`^ss4o_${integrationType}-[^\\-]+-[^\\-]+`)) - ); - if (!nameValidity) { - validationErrors.push('This index does not match the suggested naming convention.'); - setErrors(validationErrors); - } - return true; - }; - - const fetchDataSourceMappings = async ( - targetDataSource: string - ): Promise<{ [key: string]: { properties: any } } | null> => { - return http - .post('/api/console/proxy', { - query: { - path: `${targetDataSource}/_mapping`, - method: 'GET', - }, - }) - .then((response) => { - // Un-nest properties by a level for caller convenience - Object.keys(response).forEach((key) => { - response[key].properties = response[key].mappings.properties; - }); - return response; - }) - .catch((err: any) => { - console.error(err); - return null; - }); - }; - - const fetchIntegrationMappings = async ( - targetName: string - ): Promise<{ [key: string]: { template: { mappings: { properties?: any } } } } | null> => { - return http - .get(`/api/integrations/repository/${targetName}/schema`) - .then((response) => { - if (response.statusCode && response.statusCode !== 200) { - throw new Error('Failed to retrieve Integration schema', { cause: response }); - } - return response.data.mappings; - }) - .catch((err: any) => { - console.error(err); - return null; - }); - }; - - const doExistingDataSourceValidation = async (targetDataSource: string): Promise => { - const validationErrors: string[] = []; - if (!checkDataSourceName(targetDataSource, validationErrors)) { - return false; - } - const [dataSourceMappings, integrationMappings] = await Promise.all([ - fetchDataSourceMappings(targetDataSource), - fetchIntegrationMappings(integrationName), - ]); - if (!dataSourceMappings) { - validationErrors.push('Provided data stream could not be retrieved'); - setErrors(validationErrors); - return false; - } - if (!integrationMappings) { - validationErrors.push('Failed to retrieve integration schema information'); - setErrors(validationErrors); - return false; - } - const validationResult = Object.values(dataSourceMappings).every((value) => - doPropertyValidation(integrationType, value.properties, integrationMappings) - ); - if (!validationResult) { - validationErrors.push('The provided index does not match the schema'); - setErrors(validationErrors); - } - return validationResult; - }; - const formContent = () => { return (
    @@ -227,11 +82,16 @@ export function AddIntegrationFlyout(props: IntegrationFlyoutProps) { { - const validationResult = await doExistingDataSourceValidation(dataSource); - if (validationResult) { + const validationResult = await doExistingDataSourceValidation( + dataSource, + integrationName, + integrationType + ); + if (validationResult.ok) { setToast('Index name or wildcard pattern is valid', 'success'); } - setDataSourceValid(validationResult); + setDataSourceValid(validationResult.ok); + setErrors(!validationResult.ok ? validationResult.errors : []); }} disabled={dataSource.length === 0} > diff --git a/public/components/integrations/components/available_integration_overview_page.tsx b/public/components/integrations/components/available_integration_overview_page.tsx index e858daa54..360bf0db4 100644 --- a/public/components/integrations/components/available_integration_overview_page.tsx +++ b/public/components/integrations/components/available_integration_overview_page.tsx @@ -117,8 +117,7 @@ export function AvailableIntegrationOverviewPage(props: AvailableIntegrationOver http.get(`${INTEGRATIONS_BASE}/repository`).then((exists) => { setData(exists.data); - let newItems = exists.data.hits - .flatMap((hit: { labels?: string[] }) => hit.labels ?? []); + let newItems = exists.data.hits.flatMap((hit: { labels?: string[] }) => hit.labels ?? []); newItems = [...new Set(newItems)].sort().map((newItem) => { return { name: newItem, diff --git a/public/components/integrations/components/create_integration_helpers.ts b/public/components/integrations/components/create_integration_helpers.ts new file mode 100644 index 000000000..7e465c26b --- /dev/null +++ b/public/components/integrations/components/create_integration_helpers.ts @@ -0,0 +1,355 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Color, VALID_INDEX_NAME } from '../../../../common/constants/integrations'; +import { HttpSetup } from '../../../../../../src/core/public'; +import { coreRefs } from '../../../framework/core_refs'; +import { CONSOLE_PROXY, INTEGRATIONS_BASE } from '../../../../common/constants/shared'; + +type ValidationResult = { ok: true } | { ok: false; errors: string[] }; + +export interface IntegrationTemplate { + name: string; + type: string; + assets: { + savedObjects?: { + name: string; + version: string; + }; + queries?: Array<{ + name: string; + version: string; + language: string; + }>; + }; +} + +export const doTypeValidation = ( + toCheck: { type?: string; properties?: object }, + required: { type?: string; properties?: object } +): ValidationResult => { + if (!required.type) { + return { ok: true }; + } + if (required.type === 'object') { + if (Boolean(toCheck.properties)) { + return { ok: true }; + } + return { ok: false, errors: ["'object' type must have properties."] }; + } + if (required.type !== toCheck.type) { + return { ok: false, errors: [`Type mismatch: '${required.type}' and '${toCheck.type}'`] }; + } + return { ok: true }; +}; + +export const doNestedPropertyValidation = ( + toCheck: { type?: string; properties?: { [key: string]: object } }, + required: { type?: string; properties?: { [key: string]: object } } +): ValidationResult => { + const typeCheck = doTypeValidation(toCheck, required); + if (!typeCheck.ok) { + return typeCheck; + } + for (const property of Object.keys(required.properties ?? {})) { + if (!Object.hasOwn(toCheck.properties ?? {}, property)) { + return { ok: false, errors: [`Missing field '${property}'`] }; + } + // Both are safely non-null after above checks. + const nested = doNestedPropertyValidation( + toCheck.properties![property], + required.properties![property] + ); + if (!nested.ok) { + return nested; + } + } + return { ok: true }; +}; + +export const doPropertyValidation = ( + rootType: string, + dataSourceProps: { [key: string]: { properties?: any } }, + requiredMappings: { [key: string]: { template: { mappings: { properties?: any } } } } +): ValidationResult => { + // Check root object type (without dependencies) + if (!Object.hasOwn(requiredMappings, rootType)) { + // This is a configuration error for the integration. + return { ok: false, errors: ['Required mapping for integration has no root type.'] }; + } + for (const [key, value] of Object.entries( + requiredMappings[rootType].template.mappings.properties + )) { + if ( + !dataSourceProps[key] || + !doNestedPropertyValidation(dataSourceProps[key], value as any).ok + ) { + return { ok: false, errors: [`Data source is invalid at key '${key}'`] }; + } + } + // Check nested dependencies + for (const [key, value] of Object.entries(requiredMappings)) { + if (key === rootType) { + continue; + } + if ( + !dataSourceProps[key] || + !doNestedPropertyValidation(dataSourceProps[key], value.template.mappings.properties).ok + ) { + return { ok: false, errors: [`Data source is invalid at key '${key}'`] }; + } + } + return { ok: true }; +}; + +// Returns true if the data stream is a legal name. +// Appends any additional validation errors to the provided errors array. +export const checkDataSourceName = ( + targetDataSource: string, + integrationType: string +): ValidationResult => { + let errors: string[] = []; + if (!VALID_INDEX_NAME.test(targetDataSource)) { + errors = errors.concat('This is not a valid index name.'); + return { ok: false, errors }; + } + const nameValidity: boolean = new RegExp(`^ss4?o_${integrationType}-[^\\-]+-.+`).test( + targetDataSource + ); + if (!nameValidity) { + errors = errors.concat('This index does not match the suggested naming convention.'); + return { ok: false, errors }; + } + return { ok: true }; +}; + +export const fetchDataSourceMappings = async ( + targetDataSource: string, + http: HttpSetup +): Promise<{ [key: string]: { properties: any } } | null> => { + return http + .post(CONSOLE_PROXY, { + query: { + path: `${targetDataSource}/_mapping`, + method: 'GET', + }, + }) + .then((response) => { + // Un-nest properties by a level for caller convenience + Object.keys(response).forEach((key) => { + response[key].properties = response[key].mappings.properties; + }); + return response; + }) + .catch((err: any) => { + console.error(err); + return null; + }); +}; + +export const fetchIntegrationMappings = async ( + targetName: string, + http: HttpSetup +): Promise<{ [key: string]: { template: { mappings: { properties?: any } } } } | null> => { + return http + .get(`/api/integrations/repository/${targetName}/schema`) + .then((response) => { + if (response.statusCode && response.statusCode !== 200) { + throw new Error('Failed to retrieve Integration schema', { cause: response }); + } + return response.data.mappings; + }) + .catch((err: any) => { + console.error(err); + return null; + }); +}; + +export const doExistingDataSourceValidation = async ( + targetDataSource: string, + integrationName: string, + integrationType: string +): Promise => { + const http = coreRefs.http!; + const dataSourceNameCheck = checkDataSourceName(targetDataSource, integrationType); + if (!dataSourceNameCheck.ok) { + return dataSourceNameCheck; + } + const [dataSourceMappings, integrationMappings] = await Promise.all([ + fetchDataSourceMappings(targetDataSource, http), + fetchIntegrationMappings(integrationName, http), + ]); + if (!dataSourceMappings) { + return { ok: false, errors: ['Provided data stream could not be retrieved'] }; + } + if (!integrationMappings) { + return { ok: false, errors: ['Failed to retrieve integration schema information'] }; + } + const validationResult = Object.values(dataSourceMappings).every( + (value) => doPropertyValidation(integrationType, value.properties, integrationMappings).ok + ); + if (!validationResult) { + return { ok: false, errors: ['The provided index does not match the schema'] }; + } + return { ok: true }; +}; + +const createComponentMapping = async ( + componentName: string, + payload: { + template: { mappings: { _meta: { version: string } } }; + composed_of: string[]; + index_patterns: string[]; + } +): Promise<{ [key: string]: { properties: any } } | null> => { + const http = coreRefs.http!; + const version = payload.template.mappings._meta.version; + return http.post(CONSOLE_PROXY, { + body: JSON.stringify(payload), + query: { + path: `_component_template/ss4o_${componentName}-${version}-template`, + method: 'POST', + }, + }); +}; + +const createIndexMapping = async ( + componentName: string, + payload: { + template: { mappings: { _meta: { version: string } } }; + composed_of: string[]; + index_patterns: string[]; + }, + dataSourceName: string, + integration: IntegrationTemplate +): Promise<{ [key: string]: { properties: any } } | null> => { + const http = coreRefs.http!; + const version = payload.template.mappings._meta.version; + payload.index_patterns = [dataSourceName]; + return http.post(CONSOLE_PROXY, { + body: JSON.stringify(payload), + query: { + path: `_index_template/ss4o_${componentName}-${integration.name}-${version}-sample`, + method: 'POST', + }, + }); +}; + +const createDataSourceMappings = async ( + targetDataSource: string, + integrationTemplateId: string, + integration: IntegrationTemplate, + setToast: (title: string, color?: Color, text?: string | undefined) => void +): Promise => { + const http = coreRefs.http!; + const data = await http.get(`${INTEGRATIONS_BASE}/repository/${integrationTemplateId}/schema`); + let error: string | null = null; + const mappings = data.data.mappings; + mappings[integration.type].composed_of = mappings[integration.type].composed_of.map( + (componentName: string) => { + const version = mappings[componentName].template.mappings._meta.version; + return `ss4o_${componentName}-${version}-template`; + } + ); + + try { + // Create component mappings before the index mapping + // The assumption is that index mapping relies on component mappings for creation + await Promise.all( + Object.entries(mappings).map(([key, mapping]) => { + if (key === integration.type) { + return Promise.resolve(); + } + return createComponentMapping(key, mapping as any); + }) + ); + // In order to see our changes, we need to manually provoke a refresh + await http.post(CONSOLE_PROXY, { + query: { + path: '_refresh', + method: 'GET', + }, + }); + await createIndexMapping( + integration.type, + mappings[integration.type], + targetDataSource, + integration + ); + } catch (err: any) { + error = err.message; + } + + if (error !== null) { + setToast('Failure creating index template', 'danger', error); + } else { + setToast(`Successfully created index template`); + } +}; + +export async function addIntegrationRequest( + addSample: boolean, + templateName: string, + integrationTemplateId: string, + integration: IntegrationTemplate, + setToast: (title: string, color?: Color, text?: string | undefined) => void, + name?: string, + dataSource?: string +) { + const http = coreRefs.http!; + if (addSample) { + createDataSourceMappings( + `ss4o_${integration.type}-${integrationTemplateId}-*-sample`, + integrationTemplateId, + integration, + setToast + ); + name = `${integrationTemplateId}-sample`; + dataSource = `ss4o_${integration.type}-${integrationTemplateId}-sample-sample`; + } + + const response: boolean = await http + .post(`${INTEGRATIONS_BASE}/store/${templateName}`, { + body: JSON.stringify({ name, dataSource }), + }) + .then((_res) => { + setToast(`${name} integration successfully added!`, 'success'); + window.location.hash = `#/installed/${_res.data?.id}`; + return true; + }) + .catch((_err) => { + setToast( + 'Failed to load integration. Check Added Integrations table for more details', + 'danger' + ); + return false; + }); + if (!addSample || !response) { + return; + } + const data: { sampleData: unknown[] } = await http + .get(`${INTEGRATIONS_BASE}/repository/${templateName}/data`) + .then((res) => res.data) + .catch((err) => { + console.error(err); + setToast('The sample data could not be retrieved', 'danger'); + return { sampleData: [] }; + }); + const requestBody = + data.sampleData + .map((record) => `{"create": { "_index": "${dataSource}" } }\n${JSON.stringify(record)}`) + .join('\n') + '\n'; + http + .post(CONSOLE_PROXY, { + body: requestBody, + query: { + path: `${dataSource}/_bulk?refresh=wait_for`, + method: 'POST', + }, + }) + .catch((err) => { + console.error(err); + setToast('Failed to load sample data', 'danger'); + }); +} diff --git a/public/components/integrations/components/integration.tsx b/public/components/integrations/components/integration.tsx index 28257400b..c005e98d0 100644 --- a/public/components/integrations/components/integration.tsx +++ b/public/components/integrations/components/integration.tsx @@ -21,99 +21,21 @@ import { IntegrationAssets } from './integration_assets_panel'; import { AvailableIntegrationProps } from './integration_types'; import { INTEGRATIONS_BASE } from '../../../../common/constants/shared'; import { IntegrationScreenshots } from './integration_screenshots_panel'; -import { AddIntegrationFlyout } from './add_integration_flyout'; import { useToast } from '../../../../public/components/common/toast'; +import { coreRefs } from '../../../framework/core_refs'; +import { IntegrationTemplate, addIntegrationRequest } from './create_integration_helpers'; export function Integration(props: AvailableIntegrationProps) { - const { http, integrationTemplateId, chrome } = props; + const http = coreRefs.http!; + const { integrationTemplateId, chrome } = props; - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const { setToast } = useToast(); - const [integration, setIntegration] = useState({} as { name: string; type: string }); + const [integration, setIntegration] = useState({} as IntegrationTemplate); const [integrationMapping, setMapping] = useState(null); const [integrationAssets, setAssets] = useState([]); const [loading, setLoading] = useState(false); - const createComponentMapping = async ( - componentName: string, - payload: { - template: { mappings: { _meta: { version: string } } }; - composed_of: string[]; - index_patterns: string[]; - } - ): Promise<{ [key: string]: { properties: any } } | null> => { - const version = payload.template.mappings._meta.version; - return http.post('/api/console/proxy', { - body: JSON.stringify(payload), - query: { - path: `_component_template/ss4o_${componentName}-${version}-template`, - method: 'POST', - }, - }); - }; - - const createIndexMapping = async ( - componentName: string, - payload: { - template: { mappings: { _meta: { version: string } } }; - composed_of: string[]; - index_patterns: string[]; - }, - dataSourceName: string - ): Promise<{ [key: string]: { properties: any } } | null> => { - const version = payload.template.mappings._meta.version; - payload.index_patterns = [dataSourceName]; - return http.post('/api/console/proxy', { - body: JSON.stringify(payload), - query: { - path: `_index_template/ss4o_${componentName}-${integration.name}-${version}-sample`, - method: 'POST', - }, - }); - }; - - const createDataSourceMappings = async (targetDataSource: string): Promise => { - const data = await http.get(`${INTEGRATIONS_BASE}/repository/${integrationTemplateId}/schema`); - let error: string | null = null; - const mappings = data.data.mappings; - mappings[integration.type].composed_of = mappings[integration.type].composed_of.map( - (componentName: string) => { - const version = mappings[componentName].template.mappings._meta.version; - return `ss4o_${componentName}-${version}-template`; - } - ); - - try { - // Create component mappings before the index mapping - // The assumption is that index mapping relies on component mappings for creation - await Promise.all( - Object.entries(mappings).map(([key, mapping]) => { - if (key === integration.type) { - return Promise.resolve(); - } - return createComponentMapping(key, mapping as any); - }) - ); - // In order to see our changes, we need to manually provoke a refresh - await http.post('/api/console/proxy', { - query: { - path: '_refresh', - method: 'GET', - }, - }); - await createIndexMapping(integration.type, mappings[integration.type], targetDataSource); - } catch (err: any) { - error = err.message; - } - - if (error !== null) { - setToast('Failure creating index template', 'danger', error); - } else { - setToast(`Successfully created index template`); - } - }; - useEffect(() => { chrome.setBreadcrumbs([ { @@ -171,68 +93,6 @@ export function Integration(props: AvailableIntegrationProps) { }); }, [integration]); - async function addIntegrationRequest( - addSample: boolean, - templateName: string, - name?: string, - dataSource?: string - ) { - setLoading(true); - if (addSample) { - createDataSourceMappings(`ss4o_${integration.type}-${integrationTemplateId}-*-sample`); - name = `${integrationTemplateId}-sample`; - dataSource = `ss4o_${integration.type}-${integrationTemplateId}-sample-sample`; - } - - const response: boolean = await http - .post(`${INTEGRATIONS_BASE}/store/${templateName}`, { - body: JSON.stringify({ name, dataSource }), - }) - .then((_res) => { - setToast(`${name} integration successfully added!`, 'success'); - window.location.hash = `#/installed/${_res.data?.id}`; - return true; - }) - .catch((_err) => { - setToast( - 'Failed to load integration. Check Added Integrations table for more details', - 'danger' - ); - return false; - }); - if (!addSample || !response) { - setLoading(false); - return; - } - const data: { sampleData: unknown[] } = await http - .get(`${INTEGRATIONS_BASE}/repository/${templateName}/data`) - .then((res) => res.data) - .catch((err) => { - console.error(err); - setToast('The sample data could not be retrieved', 'danger'); - return { sampleData: [] }; - }); - const requestBody = - data.sampleData - .map((record) => `{"create": { "_index": "${dataSource}" } }\n${JSON.stringify(record)}`) - .join('\n') + '\n'; - http - .post('/api/console/proxy', { - body: requestBody, - query: { - path: `${dataSource}/_bulk?refresh=wait_for`, - method: 'POST', - }, - }) - .catch((err) => { - console.error(err); - setToast('Failed to load sample data', 'danger'); - }) - .finally(() => { - setLoading(false); - }); - } - const tabs = [ { id: 'assets', @@ -280,10 +140,18 @@ export function Integration(props: AvailableIntegrationProps) { {IntegrationOverview({ integration, showFlyout: () => { - setIsFlyoutVisible(true); + window.location.hash = `#/available/${integration.name}/setup`; }, - setUpSample: () => { - addIntegrationRequest(true, integrationTemplateId); + setUpSample: async () => { + setLoading(true); + await addIntegrationRequest( + true, + integration.name, + integrationTemplateId, + integration, + setToast + ); + setLoading(false); }, loading, })} @@ -299,19 +167,6 @@ export function Integration(props: AvailableIntegrationProps) { : IntegrationFields({ integration, integrationMapping })} - {isFlyoutVisible && ( - { - setIsFlyoutVisible(false); - }} - onCreate={(name, dataSource) => { - addIntegrationRequest(false, integrationTemplateId, name, dataSource); - }} - integrationName={integrationTemplateId} - integrationType={integration.type} - http={http} - /> - )} ); } diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx new file mode 100644 index 000000000..96470d45b --- /dev/null +++ b/public/components/integrations/components/setup_integration.tsx @@ -0,0 +1,435 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiBottomBar, + EuiButton, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLoadingDashboards, + EuiModal, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiProgress, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { coreRefs } from '../../../framework/core_refs'; +import { IntegrationTemplate, addIntegrationRequest } from './create_integration_helpers'; +import { useToast } from '../../../../public/components/common/toast'; +import { CONSOLE_PROXY, INTEGRATIONS_BASE } from '../../../../common/constants/shared'; +import { DATACONNECTIONS_BASE } from '../../../../common/constants/shared'; + +export interface IntegrationSetupInputs { + displayName: string; + connectionType: string; + connectionDataSource: string; +} + +interface IntegrationConfigProps { + config: IntegrationSetupInputs; + updateConfig: (updates: Partial) => void; + integration: IntegrationTemplate; +} + +// TODO support localization +const INTEGRATION_CONNECTION_DATA_SOURCE_TYPES: Map< + string, + { + title: string; + lower: string; + help: string; + } +> = new Map([ + [ + 's3', + { + title: 'Table', + lower: 'table', + help: 'Select a table to pull the data from.', + }, + ], + [ + 'index', + { + title: 'Index', + lower: 'index', + help: 'Select an index to pull the data from.', + }, + ], +]); + +const integrationConnectionSelectorItems = [ + { + value: 's3', + text: 'S3 Connection', + }, + { + value: 'index', + text: 'OpenSearch Index', + }, +]; + +const suggestDataSources = async (type: string): Promise> => { + const http = coreRefs.http!; + try { + if (type === 'index') { + const result = await http.post(CONSOLE_PROXY, { + body: '{}', + query: { + path: '_data_stream/ss4o_*', + method: 'GET', + }, + }); + return ( + result.data_streams?.map((item: { name: string }) => { + return { label: item.name }; + }) ?? [] + ); + } else if (type === 's3') { + const result = (await http.get(DATACONNECTIONS_BASE)) as Array<{ + name: string; + connector: string; + }>; + return ( + result + ?.filter((item) => item.connector === 'S3GLUE') + .map((item) => { + return { label: item.name }; + }) ?? [] + ); + } else { + console.error(`Unknown connection type: ${type}`); + return []; + } + } catch (err: any) { + console.error(err.message); + return []; + } +}; + +const runQuery = async ( + query: string, + trackProgress: (step: number) => void +): Promise> => { + // Used for polling + const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }; + + try { + const http = coreRefs.http!; + const queryId = ( + await http.post(CONSOLE_PROXY, { + body: JSON.stringify({ query, lang: 'sql' }), + query: { + path: '_plugins/_async_query', + method: 'POST', + }, + }) + ).queryId; + while (true) { + const poll = await http.post(CONSOLE_PROXY, { + body: '{}', + query: { + path: '_plugins/_async_query/' + queryId, + method: 'GET', + }, + }); + if (poll.status === 'PENDING') { + trackProgress(1); + } else if (poll.status === 'RUNNING') { + trackProgress(2); + } else if (poll.status === 'SUCCESS') { + trackProgress(3); + return { ok: true, value: poll }; + } else if (poll.status === 'FAILURE') { + return { ok: false, error: new Error('FAILURE status', { cause: poll }) }; + } + await sleep(3000); + } + } catch (err: any) { + console.error(err); + return { ok: false, error: err }; + } +}; + +export function SetupIntegrationForm({ + config, + updateConfig, + integration, +}: IntegrationConfigProps) { + const connectionType = INTEGRATION_CONNECTION_DATA_SOURCE_TYPES.get(config.connectionType)!; + + const [dataSourceSuggestions, setDataSourceSuggestions] = useState( + [] as Array<{ label: string }> + ); + const [isSuggestionsLoading, setIsSuggestionsLoading] = useState(true); + useEffect(() => { + const updateDataSources = async () => { + const data = await suggestDataSources(config.connectionType); + setDataSourceSuggestions(data); + setIsSuggestionsLoading(false); + }; + + setIsSuggestionsLoading(true); + updateDataSources(); + }, [config.connectionType]); + + return ( + + +

    Set Up Integration

    +
    + + +

    Integration Details

    +
    + + + updateConfig({ displayName: event.target.value })} + /> + + + +

    Integration Connection

    +
    + + + { + const copy: { value: string; text: string; disabled?: boolean } = { ...item }; + switch (item.value) { + case 's3': + copy.disabled = !Object.hasOwn(integration.assets ?? {}, 'queries'); + return copy; + case 'index': + copy.disabled = !Object.hasOwn(integration.assets ?? {}, 'savedObjects'); + return copy; + default: + return copy; + } + })} + value={config.connectionType} + onChange={(event) => + updateConfig({ connectionType: event.target.value, connectionDataSource: '' }) + } + /> + + + { + if (selected.length === 0) { + updateConfig({ connectionDataSource: '' }); + } else { + updateConfig({ connectionDataSource: selected[0].label }); + } + }} + selectedOptions={[{ label: config.connectionDataSource }]} + singleSelection={{ asPlainText: true }} + /> + +
    + ); +} + +export function SetupBottomBar({ + config, + integration, + loading, + setLoading, + loadingProgress, + setProgress, +}: { + config: IntegrationSetupInputs; + integration: IntegrationTemplate; + loading: boolean; + setLoading: (loading: boolean) => void; + loadingProgress: number; + setProgress: (updater: number | ((progress: number) => void)) => void; +}) { + const { setToast } = useToast(); + + return ( + + + + { + // TODO evil hack because props aren't set up + let hash = window.location.hash; + hash = hash.trim(); + hash = hash.substring(0, hash.lastIndexOf('/setup')); + window.location.hash = hash; + }} + > + Discard + + + + { + setLoading(true); + + if (config.connectionType === 'index') { + await addIntegrationRequest( + false, + integration.name, + config.displayName, + integration, + setToast, + config.displayName, + config.connectionDataSource + ); + setProgress((progress) => progress + 1); + } else if (config.connectionType === 's3') { + const http = coreRefs.http!; + + const assets = await http.get( + `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` + ); + setProgress((progress) => progress + 1); + + // Queries must exist because we disable s3 if they're not present + for (const query of assets.data.queries!) { + const queryStr = query.query.replace('${TABLE}', config.connectionDataSource); + const currProgress = loadingProgress; // Need a frozen copy for getting accurate query steps + const result = await runQuery(queryStr, (step) => + setProgress(currProgress + step) + ); + if (!result.ok) { + console.error('Query failed', result.error); + setLoading(false); + setToast('Something went wrong.', 'danger'); + return; + } + } + // Once everything is ready, add the integration to the new datasource as usual + // TODO determine actual values here after more about queries is known + await addIntegrationRequest( + false, + integration.name, + config.displayName, + integration, + setToast, + config.displayName, + config.connectionDataSource + ); + setProgress((progress) => progress + 1); + } else { + console.error('Invalid data source type'); + } + setLoading(false); + }} + > + Add Integration + + + + + ); +} + +export function LoadingPage({ value, max }: { value: number; max: number }) { + return ( + <> + + + + + + + +

    Adding Integration

    +
    +
    + + + This may take a few minutes. The integration and assets are being added. + + +
    + + + + ); +} + +export function SetupIntegrationPage({ integration }: { integration: string }) { + const [integConfig, setConfig] = useState({ + displayName: `${integration} Integration`, + connectionType: 'index', + connectionDataSource: '', + } as IntegrationSetupInputs); + + const [template, setTemplate] = useState({ + name: integration, + type: '', + assets: {}, + } as IntegrationTemplate); + + const [showLoading, setShowLoading] = useState(false); + const [loadingProgress, setLoadingProgress] = useState(0); + + useEffect(() => { + const getTemplate = async () => { + const http = coreRefs.http!; + const value = await http.get(INTEGRATIONS_BASE + `/repository/${integration}`); + setTemplate(value.data); + }; + getTemplate(); + }, [integration]); + + const updateConfig = (updates: Partial) => + setConfig(Object.assign({}, integConfig, updates)); + const maxProgress = 2 + 3 * (template.assets?.queries?.length ?? 0); + + return ( + + + + + {showLoading ? ( + + ) : ( + + )} + + + + + + ); +} diff --git a/public/components/integrations/home.tsx b/public/components/integrations/home.tsx index 3fa85b98d..2782b92a8 100644 --- a/public/components/integrations/home.tsx +++ b/public/components/integrations/home.tsx @@ -3,16 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React from 'react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { EuiGlobalToastList } from '@elastic/eui'; -import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; -import { Integration } from './components/integration'; import { TraceAnalyticsCoreDeps } from '../trace_analytics/home'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { AvailableIntegrationOverviewPage } from './components/available_integration_overview_page'; import { AddedIntegrationOverviewPage } from './components/added_integration_overview_page'; import { AddedIntegration } from './components/added_integration'; +import { SetupIntegrationPage } from './components/setup_integration'; +import { Integration } from './components/integration'; export type AppAnalyticsCoreDeps = TraceAnalyticsCoreDeps; @@ -54,7 +53,7 @@ export const Home = (props: HomeProps) => { /> ( { /> )} /> + ( + + )} + />
    diff --git a/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx b/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx deleted file mode 100644 index db07da1c5..000000000 --- a/public/components/notebooks/components/helpers/__tests__/sampleZeppelinNotebooks.tsx +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// Sample notebook with all input and output -export const sampleNotebook1 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - results: { - code: 'SUCCESS', - msg: [ - { - type: 'HTML', - data: - '
    \n

    Hi Everyone

    \n
      \n
    • Here’s a demo on OpenSearch Dashboards Notebooks
    • \n
    • You may use the top left buttons to play around with notebooks and Paragraphs
    • \n
    \n\n
    ', - }, - ], - }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - id: 'paragraph_1596519508360_932236116', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'VISUALIZATION', - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Parsed Output of sample notebook1 -export const sampleParsedParagraghs1 = [ - { - uniqueId: 'paragraph_1596519508360_932236116', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 1, - inp: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - lang: 'text/x-', - editorLanguage: '', - typeOut: ['HTML'], - out: [ - '
    \n

    Hi Everyone

    \n
      \n
    • Here’s a demo on OpenSearch Dashboards Notebooks
    • \n
    • You may use the top left buttons to play around with notebooks and Paragraphs
    • \n
    \n\n
    ', - ], - }, - { - uniqueId: 'paragraph_1597958728587_1310320520', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: true, - vizObjectInput: - '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - id: 2, - inp: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, -]; - -// Sample notebook with all input and cleared outputs -export const sampleNotebook2 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - id: 'paragraph_1596519508360_932236116', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'Paragraph inserted', - text: '%md\n\n## Greetings!\n* Yay! you may import and export me ', - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_1715920734', - id: 'paragraph_1596742076640_674206137', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'Paragraph inserted', - text: - "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_931410594', - id: 'paragraph_1596524302932_2112910756', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - { - title: 'VISUALIZATION', - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Parsed Output of sample notebook2 -export const sampleParsedParagraghs2 = [ - { - uniqueId: 'paragraph_1596519508360_932236116', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 1, - inp: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1596742076640_674206137', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 2, - inp: '%md\n\n## Greetings!\n* Yay! you may import and export me ', - lang: 'text/x-md', - editorLanguage: 'md', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1596524302932_2112910756', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: false, - vizObjectInput: '', - id: 3, - inp: - "%md\n\n### Let's use Visualization API with dashboard container to embed Visualizations in notebooks\n2. **Unpin** the container to *edit the size* or *delete it*\n3. **Refresh** the container after *date is changed*", - lang: 'text/x-md', - editorLanguage: 'md', - typeOut: [], - out: [], - }, - { - uniqueId: 'paragraph_1597958728587_1310320520', - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: true, - vizObjectInput: - '{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - id: 4, - inp: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - lang: 'text/x-', - editorLanguage: '', - typeOut: [], - out: [], - }, -]; - -// Sample notebook with no paragraph Id -export const sampleNotebook3 = { - paragraphs: [ - { - text: - "%md \n\n### Hi Everyone\n* Here's a demo on **OpenSearch Dashboards Notebooks**\n* You may use the top left buttons to play around with notebooks and Paragraphs", - user: 'anonymous', - dateUpdated: '2020-08-20 21:15:04.590', - config: {}, - settings: { params: {}, forms: {} }, - results: { - code: 'SUCCESS', - msg: [ - { - type: 'HTML', - data: - '
    \n

    Hi Everyone

    \n
      \n
    • Here’s a demo on OpenSearch Dashboards Notebooks
    • \n
    • You may use the top left buttons to play around with notebooks and Paragraphs
    • \n
    \n\n
    ', - }, - ], - }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958104590_901298942', - dateCreated: '2020-08-20 21:15:04.590', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Sample notebook with no VISUALIZAITON title -export const sampleNotebook4 = { - paragraphs: [ - { - text: - '%sh #vizobject:{"viewMode":"view","panels":{"1":{"gridData":{"x":15,"y":0,"w":20,"h":20,"i":"1"},"type":"visualization","explicitInput":{"id":"1","savedObjectId":"06cf9c40-9ee8-11e7-8711-e7a007dcef99"}}},"isFullScreenMode":false,"filters":[],"useMargins":false,"id":"iab4eaba1-e32b-11ea-aac8-99f209533253","timeRange":{"to":"2020-08-20T21:25:28.538Z","from":"2020-07-21T21:25:28.538Z"},"title":"embed_viz_iab4eaba1-e32b-11ea-aac8-99f209533253","query":{"query":"","language":"lucene"},"refreshConfig":{"pause":true,"value":15}}', - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; - -// Sample notebook with no input and output -export const sampleNotebook5 = { - paragraphs: [ - { - user: 'anonymous', - dateUpdated: '2020-08-20 21:25:28.588', - config: {}, - settings: { params: {}, forms: {} }, - apps: [], - runtimeInfos: {}, - progressUpdateIntervalMs: 500, - jobName: 'paragraph_1597958728587_1310320520', - id: 'paragraph_1597958728587_1310320520', - dateCreated: '2020-08-20 21:25:28.587', - status: 'READY', - }, - ], - name: 'Embed Vizualization', - id: '2FJH8PW8K', - defaultInterpreterGroup: 'spark', - version: '0.9.0-preview2', - noteParams: {}, - noteForms: {}, - angularObjects: {}, - config: { isZeppelinNotebookCronEnable: false }, - info: {}, -}; diff --git a/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx b/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx deleted file mode 100644 index e6c5b351e..000000000 --- a/public/components/notebooks/components/helpers/__tests__/zeppelin_parser.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { zeppelinParagraphParser } from '../zeppelin_parser'; -import { - sampleNotebook1, - sampleNotebook2, - sampleNotebook3, - sampleNotebook4, - sampleNotebook5, - sampleParsedParagraghs1, - sampleParsedParagraghs2, -} from './sampleZeppelinNotebooks'; - -// Perfect schema -describe('Testing Zeppelin backend parser function with perfect schema', () => { - test('zeppelinParagraphParserTest1', (done) => { - const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook1.paragraphs); - const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook2.paragraphs); - const parsedParagraphs3 = zeppelinParagraphParser([]); - expect(parsedParagraphs1).toEqual(sampleParsedParagraghs1); - expect(parsedParagraphs2).toEqual(sampleParsedParagraghs2); - expect(parsedParagraphs3).toEqual([]); - done(); - }); -}); - -// Issue in schema -describe('Testing default backend parser function with wrong schema', () => { - test('zeppelinParagraphParserTest2', (done) => { - expect(() => { - const parsedParagraphs1 = zeppelinParagraphParser(sampleNotebook3.paragraphs); - }).toThrow(Error); - expect(() => { - const parsedParagraphs2 = zeppelinParagraphParser(sampleNotebook4.paragraphs); - }).toThrow(Error); - expect(() => { - const parsedParagraphs3 = zeppelinParagraphParser(sampleNotebook5.paragraphs); - }).toThrow(Error); - done(); - }); -}); diff --git a/public/components/notebooks/components/helpers/default_parser.tsx b/public/components/notebooks/components/helpers/default_parser.tsx index c38dad3cd..7647d80f3 100644 --- a/public/components/notebooks/components/helpers/default_parser.tsx +++ b/public/components/notebooks/components/helpers/default_parser.tsx @@ -3,21 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ParaType } from '../../../../../common/types/notebooks'; - // Get the type of output and result in a default notebook paragraph // Param: Default Backend Paragraph const parseOutput = (paraObject: any) => { try { - let outputType = []; - let result = []; - paraObject.output.map((output: { outputType: string; result: string }) => { - outputType.push(output.outputType); - result.push(output.result); - }); return { - outputType: outputType, - outputData: result, + outputType: paraObject.output.map(({ outputType }) => outputType), + outputData: paraObject.output.map(({ result }) => result), }; } catch (error) { return { @@ -46,7 +38,7 @@ const parseInputType = (paraObject: any) => { const parseVisualization = (paraObject: any) => { try { if (paraObject.input.inputType.includes('VISUALIZATION')) { - let vizContent = paraObject.input.inputText; + const vizContent = paraObject.input.inputText; const startDate = new Date(); startDate.setDate(startDate.getDate() - 30); let visStartTime = startDate.toISOString(); @@ -76,43 +68,42 @@ const parseVisualization = (paraObject: any) => { } }; +const parseBackendParagraph = (paraObject, index) => { + const codeLanguage = parseInputType(paraObject); + const vizParams = parseVisualization(paraObject); + const message = parseOutput(paraObject); + + return { + uniqueId: paraObject.id, + isRunning: false, + inQueue: false, + isSelected: false, + isInputHidden: false, + isOutputHidden: false, + showAddPara: false, + isVizualisation: vizParams.isViz, + vizObjectInput: vizParams.VizObject, + id: index + 1, + inp: paraObject.input.inputText || '', + lang: 'text/x-' + codeLanguage, + editorLanguage: codeLanguage, + typeOut: message.outputType, + out: message.outputData, + isInputExpanded: false, + isOutputStale: false, + paraRef: undefined, + paraDivRef: undefined, + visStartTime: vizParams.visStartTime, + visEndTime: vizParams.visEndTime, + visSavedObjId: vizParams.visSavedObjId, + }; +}; + // Placeholder for default parser // Param: Default Backend Paragraph export const defaultParagraphParser = (defaultBackendParagraphs: any) => { - let parsedPara: Array = []; try { - defaultBackendParagraphs.map((paraObject: any, index: number) => { - const codeLanguage = parseInputType(paraObject); - const vizParams = parseVisualization(paraObject); - const message = parseOutput(paraObject); - - let tempPara: ParaType = { - uniqueId: paraObject.id, - isRunning: false, - inQueue: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: vizParams.isViz, - vizObjectInput: vizParams.VizObject, - id: index + 1, - inp: paraObject.input.inputText || '', - lang: 'text/x-' + codeLanguage, - editorLanguage: codeLanguage, - typeOut: message.outputType, - out: message.outputData, - isInputExpanded: false, - isOutputStale: false, - paraRef: undefined, - paraDivRef: undefined, - visStartTime: vizParams.visStartTime, - visEndTime: vizParams.visEndTime, - visSavedObjId: vizParams.visSavedObjId, - }; - parsedPara.push(tempPara); - }); - return parsedPara; + return defaultBackendParagraphs.map(parseBackendParagraph); } catch (error) { throw new Error('Parsing Paragraph Issue ' + error); } diff --git a/public/components/notebooks/components/helpers/zeppelin_parser.tsx b/public/components/notebooks/components/helpers/zeppelin_parser.tsx deleted file mode 100644 index f2293c3a7..000000000 --- a/public/components/notebooks/components/helpers/zeppelin_parser.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -/* This file contains parsing functions - * These functions have to be changed based on backend configuration - * If backend changes the incoming paragraph structures may change, so parsing adapts to it - */ - -import { ParaType } from '../../../common'; - -const visualizationPrefix = '%sh #vizobject:'; -const observabilityVisualizationPrefix = '%sh #observabilityviz:'; - -const langSupport = { - '%sh': 'shell', - '%md': 'md', - '%python': 'python', - '%opensearchsql': 'sql', - '%elasticsearch': 'json', -}; - -// Get the coding language from a Zeppelin paragraph input -// Param: textHeader-> header on a Zeppelin paragraph example "%md" -const parseCodeLanguage = (textHeader: string) => { - const codeLanguage = langSupport[textHeader]; - return codeLanguage || ''; -}; - -// Get the type of output message from a Zeppelin paragraph -// Param: Zeppelin Paragraph -const parseMessage = (paraObject: any) => { - try { - let mtype = []; - let mdata = []; - paraObject.results.msg.map((msg: { type: string; data: string }) => { - mtype.push(msg.type); - mdata.push(msg.data); - }); - return { - outputType: mtype, - outputData: mdata, - }; - } catch (error) { - return { - outputType: [], - outputData: [], - }; - } -}; - -// Get the type of output message from a Zeppelin paragraph -// Param: Zeppelin Paragraph -const parseText = (paraObject: any) => { - if ('text' in paraObject) { - return paraObject.text; - } else { - throw new Error('Input text parse issue'); - } -}; - -// Get the visualization from a Zeppelin Paragraph input -// All Visualizations in Zeppelin are stored as shell comment -> "%sh #vizobject:" -// TODO: This is a workaround need to look for better solutions -// Param: Zeppelin Paragraph -const parseVisualization = (paraObject: any) => { - let vizContent = ''; - if ( - paraObject.hasOwnProperty('text') && - paraObject.text.substring(0, 15) === visualizationPrefix - ) { - if (paraObject.title !== 'VISUALIZATION') { - throw new Error('Visualization parse issue'); - } - vizContent = paraObject.text.substring(15); - return { - isViz: true, - VizObject: vizContent, - }; - } - - if ( - paraObject.hasOwnProperty('text') && - paraObject.text.substring(0, 22) === observabilityVisualizationPrefix - ) { - if (paraObject.title !== 'OBSERVABILITY_VISUALIZATION') { - throw new Error('Visualization parse issue'); - } - vizContent = paraObject.text.substring(22); - return { - isViz: true, - VizObject: vizContent, - }; - } - - return { - isViz: false, - VizObject: vizContent, - }; -}; - -// This parser is used to get paragraph id -// Param: Zeppelin Paragraph -const parseId = (paraObject: any) => { - if ('id' in paraObject) { - return paraObject.id; - } else { - throw new Error('Id not found in paragraph'); - } -}; - -// This parser helps to convert Zeppelin paragraphs to a common ParaType format -// This parsing makes any backend notebook compatible with notebooks plugin -export const zeppelinParagraphParser = (zeppelinBackendParagraphs: any) => { - let parsedPara: Array = []; - try { - zeppelinBackendParagraphs.map((paraObject: ParaType, index: number) => { - const paragraphId = parseId(paraObject); - const vizParams = parseVisualization(paraObject); - const inputParam = parseText(paraObject); - const codeLanguage = parseCodeLanguage(inputParam.split('\n')[0].split('.')[0]); - const message = parseMessage(paraObject); - - let tempPara = { - uniqueId: paragraphId, - isRunning: false, - inQueue: false, - ishovered: false, - isSelected: false, - isInputHidden: false, - isOutputHidden: false, - showAddPara: false, - isVizualisation: vizParams.isViz, - vizObjectInput: vizParams.VizObject, - id: index + 1, - inp: inputParam, - lang: 'text/x-' + codeLanguage, - editorLanguage: codeLanguage, - typeOut: message.outputType, - out: message.outputData, - }; - parsedPara.push(tempPara); - }); - return parsedPara; - } catch (error) { - throw new Error('Parsing Paragraph Issue ' + error); - } -}; diff --git a/public/components/notebooks/components/notebook.tsx b/public/components/notebooks/components/notebook.tsx index 710a70c06..429dabad9 100644 --- a/public/components/notebooks/components/notebook.tsx +++ b/public/components/notebooks/components/notebook.tsx @@ -30,11 +30,7 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { ChromeBreadcrumb, CoreStart } from '../../../../../../src/core/public'; import { DashboardStart } from '../../../../../../src/plugins/dashboard/public'; -import { - CREATE_NOTE_MESSAGE, - NOTEBOOKS_API_PREFIX, - NOTEBOOKS_SELECTED_BACKEND, -} from '../../../../common/constants/notebooks'; +import { CREATE_NOTE_MESSAGE, NOTEBOOKS_API_PREFIX } from '../../../../common/constants/notebooks'; import { UI_DATE_FORMAT } from '../../../../common/constants/shared'; import { ParaType } from '../../../../common/types/notebooks'; import { GenerateReportLoadingModal } from './helpers/custom_modals/reporting_loading_modal'; @@ -45,7 +41,6 @@ import { contextMenuViewReports, generateInContextReport, } from './helpers/reporting_context_menu_helper'; -import { zeppelinParagraphParser } from './helpers/zeppelin_parser'; import { Paragraphs } from './paragraph_components/paragraphs'; const panelStyles: CSS.Properties = { float: 'left', @@ -89,8 +84,7 @@ type NotebookState = { dateCreated: string; dateModified: string; paragraphs: any; // notebook paragraphs fetched from API - parsedPara: Array; // paragraphs parsed to a common format - vizPrefix: string; // prefix for visualizations in Zeppelin Adaptor + parsedPara: ParaType[]; // paragraphs parsed to a common format isAddParaPopoverOpen: boolean; isParaActionsPopoverOpen: boolean; isNoteActionsPopoverOpen: boolean; @@ -112,7 +106,6 @@ export class Notebook extends Component { dateModified: '', paragraphs: [], parsedPara: [], - vizPrefix: '', isAddParaPopoverOpen: false, isParaActionsPopoverOpen: false, isNoteActionsPopoverOpen: false, @@ -138,20 +131,12 @@ export class Notebook extends Component { // parse paragraphs based on backend parseParagraphs = (paragraphs: any[]): ParaType[] => { try { - let parsedPara; - // @ts-ignore - if (NOTEBOOKS_SELECTED_BACKEND === 'ZEPPELIN') { - parsedPara = zeppelinParagraphParser(paragraphs); - this.setState({ vizPrefix: '%sh #vizobject:' }); - } else { - parsedPara = defaultParagraphParser(paragraphs); - } - parsedPara.forEach((para: ParaType) => { - para.isInputExpanded = this.state.selectedViewId === 'input_only'; - para.paraRef = React.createRef(); - para.paraDivRef = React.createRef(); - }); - return parsedPara; + return defaultParagraphParser(paragraphs).map((para) => ({ + ...para, + isInputExpanded: this.state.selectedViewId === 'input_only', + paraRef: React.createRef(), + paraDivRef: React.createRef(), + })); } catch (err) { this.props.setToast( 'Error parsing paragraphs, please make sure you have the correct permission.', @@ -185,8 +170,7 @@ export class Notebook extends Component { paragraphSelector = (index: number) => { let parsedPara = this.state.parsedPara; this.state.parsedPara.map((_: ParaType, idx: number) => { - if (index === idx) parsedPara[idx].isSelected = true; - else parsedPara[idx].isSelected = false; + parsedPara[idx].isSelected = index === idx; }); this.setState({ parsedPara }); }; @@ -478,22 +462,13 @@ export class Notebook extends Component { }; // Backend call to update and run contents of paragraph - updateRunParagraph = ( - para: ParaType, - index: number, - vizObjectInput?: string, - paraType?: string - ) => { + updateRunParagraph = (para: ParaType, index: number) => { this.showParagraphRunning(index); - if (vizObjectInput) { - para.inp = this.state.vizPrefix + vizObjectInput; // "%sh check" - } const paraUpdateObject = { noteId: this.props.openedNoteId, paragraphId: para.uniqueId, paragraphInput: para.inp, - paragraphType: paraType || '', }; return this.props.http diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap index 4b25a19b5..db4ecbdad 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap @@ -220,6 +220,9 @@ exports[`Service breakdown panel component renders service breakdown panel 1`] = "yaxis": Object { "rangemode": "normal", "showgrid": true, + "title": Object { + "text": "Count", + }, "zeroline": false, }, } diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap index 9c5fa0f37..b2f3f86b1 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Veritcal Bar component Renders veritcal bar component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -691,7 +691,7 @@ exports[`Veritcal Bar component Renders veritcal bar component 1`] = ` "bargroupgap": 0.030000000000000027, "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", @@ -749,7 +749,7 @@ exports[`Veritcal Bar component Renders veritcal bar component 1`] = ` "bargroupgap": 0.030000000000000027, "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap index 4acd7cae0..bb2075dc0 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Gauge component Renders gauge component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap index 040fffaf5..0dd783bda 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Heatmap component Renders heatmap component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -703,7 +703,7 @@ exports[`Heatmap component Renders heatmap component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -756,7 +756,7 @@ exports[`Heatmap component Renders heatmap component 1`] = ` "autosize": true, "barmode": "stack", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap index ea0daf6b3..09da5b8d3 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Histogram component Renders histogram component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -704,7 +704,7 @@ exports[`Histogram component Renders histogram component 1`] = ` Object { "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "legend": Object { @@ -760,7 +760,7 @@ exports[`Histogram component Renders histogram component 1`] = ` "autosize": true, "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", @@ -785,6 +785,9 @@ exports[`Histogram component Renders histogram component 1`] = ` "yaxis": Object { "rangemode": "normal", "showgrid": true, + "title": Object { + "text": "Count", + }, "zeroline": false, }, } diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap index a9007af59..0f64e77cb 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Horizontal bar component Renders horizontal bar component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -691,7 +691,7 @@ exports[`Horizontal bar component Renders horizontal bar component 1`] = ` "bargroupgap": 0.030000000000000027, "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", @@ -749,7 +749,7 @@ exports[`Horizontal bar component Renders horizontal bar component 1`] = ` "bargroupgap": 0.030000000000000027, "barmode": "group", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap index fce2a2ea0..30e1ff271 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Line component Renders line component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -690,7 +690,7 @@ exports[`Line component Renders line component 1`] = ` Object { "autosize": true, "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "legend": Object { @@ -742,7 +742,7 @@ exports[`Line component Renders line component 1`] = ` "autosize": true, "barmode": "stack", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap index ee792c625..0d2c4eea3 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Metrics component Renders Metrics component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -618,7 +618,7 @@ exports[`Metrics component Renders Metrics component 1`] = ` "layout": Object { "colorway": Array [ "#3CA1C7", - "#8C55A3", + "#54B399", "#DB748A", "#F2BE4B", "#68CCC2", diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap index 5545e2bab..3f90bb26b 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Pie component Renders pie component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -679,7 +679,7 @@ exports[`Pie component Renders pie component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "grid": Object { "columns": 0, @@ -726,7 +726,7 @@ exports[`Pie component Renders pie component 1`] = ` "autosize": true, "barmode": "stack", "colorway": Array [ - "#8C55A3", + "#54B399", ], "grid": Object { "columns": 0, @@ -766,6 +766,9 @@ exports[`Pie component Renders pie component 1`] = ` "yaxis": Object { "rangemode": "normal", "showgrid": true, + "title": Object { + "text": "Count", + }, "zeroline": false, }, } diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap index bf22d5b9e..980c218b6 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Treemap component Renders treemap component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -716,7 +716,7 @@ exports[`Treemap component Renders treemap component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -783,7 +783,7 @@ exports[`Treemap component Renders treemap component 1`] = ` "autosize": true, "barmode": "stack", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", @@ -810,6 +810,9 @@ exports[`Treemap component Renders treemap component 1`] = ` "yaxis": Object { "rangemode": "normal", "showgrid": true, + "title": Object { + "text": "Count", + }, "zeroline": false, }, } diff --git a/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap b/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap index af24195dd..5ed2e0512 100644 --- a/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap +++ b/public/components/visualizations/plotly/__tests__/__snapshots__/plotly.test.tsx.snap @@ -36,7 +36,7 @@ exports[`Ploty base component Renders Ploty base component 1`] = ` layout={ Object { "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "margin": Object { @@ -101,7 +101,7 @@ exports[`Ploty base component Renders Ploty base component 1`] = ` "autosize": true, "barmode": "stack", "colorway": Array [ - "#8C55A3", + "#54B399", ], "height": 220, "hovermode": "closest", diff --git a/public/components/visualizations/plotly/plot.tsx b/public/components/visualizations/plotly/plot.tsx index bf1a61642..0755f6424 100644 --- a/public/components/visualizations/plotly/plot.tsx +++ b/public/components/visualizations/plotly/plot.tsx @@ -47,6 +47,9 @@ export function Plt(props: PltProps) { automargin: true, }, yaxis: { + title: { + text: 'Count', + }, showgrid: true, zeroline: false, rangemode: 'normal', diff --git a/public/framework/core_refs.ts b/public/framework/core_refs.ts index dd2367f19..16f04b3ab 100644 --- a/public/framework/core_refs.ts +++ b/public/framework/core_refs.ts @@ -9,7 +9,7 @@ * GitHub history for details. */ -import { HttpStart, IToasts } from '../../../../src/core/public'; +import { ApplicationStart, ChromeStart, HttpStart, IToasts } from '../../../../src/core/public'; import { SavedObjectsClientContract } from '../../../../src/core/public'; import PPLService from '../services/requests/ppl'; @@ -20,6 +20,8 @@ class CoreRefs { public savedObjectsClient?: SavedObjectsClientContract; public pplService?: PPLService; public toasts?: IToasts; + public chrome?: ChromeStart; + public application?: ApplicationStart; private constructor() { // ... } diff --git a/public/framework/redux/store/shared_state.ts b/public/framework/redux/store/shared_state.ts index ee041c9db..b073d7348 100644 --- a/public/framework/redux/store/shared_state.ts +++ b/public/framework/redux/store/shared_state.ts @@ -3,7 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { htmlIdGenerator } from '@elastic/eui'; -import { TAB_ID_TXT_PFX } from '../../../../common/constants/explorer'; - -export const initialTabId: string = htmlIdGenerator(TAB_ID_TXT_PFX)(); +export const initialTabId: string = 'OBSERVABILITY_DEFAULT_TAB'; diff --git a/public/plugin.ts b/public/plugin.ts index 17144abf1..9ac1fe588 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -255,6 +255,23 @@ export class ObservabilityPlugin mount: appMountWithStartPage('integrations'), }); + core.application.register({ + id: observabilityDataConnectionsID, + title: observabilityDataConnectionsTitle, + category: DEFAULT_APP_CATEGORIES.management, + order: observabilityDataConnectionsPluginOrder, + mount: appMountWithStartPage('dataconnections'), + }); + + setupDeps.managementOverview?.register({ + id: observabilityDataConnectionsID, + title: observabilityDataConnectionsTitle, + order: 9070, + description: i18n.translate('observability.dataconnectionsDescription', { + defaultMessage: 'Manage compatible data connections with OpenSearch Dashboards.', + }), + }); + const embeddableFactory = new ObservabilityEmbeddableFactoryDefinition(async () => ({ getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, diff --git a/public/types.ts b/public/types.ts index 4b37b9c06..4b6cd96a4 100644 --- a/public/types.ts +++ b/public/types.ts @@ -8,6 +8,7 @@ import { SavedObjectsClient } from '../../../src/core/server'; import { DashboardStart } from '../../../src/plugins/dashboard/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { ManagementOverViewPluginSetup } from '../../../src/plugins/management_overview/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; @@ -25,6 +26,7 @@ export interface SetupDependencies { visualizations: VisualizationsSetup; data: DataPublicPluginSetup; uiActions: UiActionsStart; + managementOverview?: ManagementOverViewPluginSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/server/adaptors/integrations/__test__/builder.test.ts b/server/adaptors/integrations/__test__/builder.test.ts index 81af1d6f6..33aff3497 100644 --- a/server/adaptors/integrations/__test__/builder.test.ts +++ b/server/adaptors/integrations/__test__/builder.test.ts @@ -5,7 +5,7 @@ import { SavedObjectsClientContract } from '../../../../../../src/core/server'; import { IntegrationInstanceBuilder } from '../integrations_builder'; -import { Integration } from '../repository/integration'; +import { IntegrationReader } from '../repository/integration'; const mockSavedObjectsClient: SavedObjectsClientContract = ({ bulkCreate: jest.fn(), @@ -16,7 +16,7 @@ const mockSavedObjectsClient: SavedObjectsClientContract = ({ update: jest.fn(), } as unknown) as SavedObjectsClientContract; -const sampleIntegration: Integration = ({ +const sampleIntegration: IntegrationReader = ({ deepCheck: jest.fn().mockResolvedValue(true), getAssets: jest.fn().mockResolvedValue({ savedObjects: [ @@ -34,7 +34,7 @@ const sampleIntegration: Integration = ({ name: 'integration-template', type: 'integration-type', }), -} as unknown) as Integration; +} as unknown) as IntegrationReader; describe('IntegrationInstanceBuilder', () => { let builder: IntegrationInstanceBuilder; @@ -93,13 +93,23 @@ describe('IntegrationInstanceBuilder', () => { ], }; - // Mock the implementation of the methods in the Integration class - sampleIntegration.deepCheck = jest.fn().mockResolvedValue(true); - sampleIntegration.getAssets = jest.fn().mockResolvedValue({ savedObjects: remappedAssets }); - sampleIntegration.getConfig = jest.fn().mockResolvedValue({ + const mockTemplate: Partial = { name: 'integration-template', type: 'integration-type', - }); + assets: { + savedObjects: { + name: 'assets', + version: '1.0.0', + }, + }, + }; + + // Mock the implementation of the methods in the Integration class + sampleIntegration.deepCheck = jest.fn().mockResolvedValue({ ok: true, value: mockTemplate }); + sampleIntegration.getAssets = jest + .fn() + .mockResolvedValue({ ok: true, value: { savedObjects: remappedAssets } }); + sampleIntegration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: mockTemplate }); // Mock builder sub-methods const remapIDsSpy = jest.spyOn(builder, 'remapIDs'); @@ -121,22 +131,24 @@ describe('IntegrationInstanceBuilder', () => { dataSource: 'instance-datasource', name: 'instance-name', }; - sampleIntegration.deepCheck = jest.fn().mockResolvedValue(false); + sampleIntegration.deepCheck = jest + .fn() + .mockResolvedValue({ ok: false, error: new Error('Mock error') }); - await expect(builder.build(sampleIntegration, options)).rejects.toThrowError( - 'Integration is not valid' - ); + await expect(builder.build(sampleIntegration, options)).rejects.toThrowError('Mock error'); }); - it('should reject with an error if getAssets throws an error', async () => { + it('should reject with an error if getAssets rejects', async () => { const options = { dataSource: 'instance-datasource', name: 'instance-name', }; const errorMessage = 'Failed to get assets'; - sampleIntegration.deepCheck = jest.fn().mockResolvedValue(true); - sampleIntegration.getAssets = jest.fn().mockRejectedValue(new Error(errorMessage)); + sampleIntegration.deepCheck = jest.fn().mockResolvedValue({ ok: true, value: {} }); + sampleIntegration.getAssets = jest + .fn() + .mockResolvedValue({ ok: false, error: new Error(errorMessage) }); await expect(builder.build(sampleIntegration, options)).rejects.toThrowError(errorMessage); }); @@ -153,22 +165,14 @@ describe('IntegrationInstanceBuilder', () => { }, ]; const errorMessage = 'Failed to post assets'; - sampleIntegration.deepCheck = jest.fn().mockResolvedValue(true); - sampleIntegration.getAssets = jest.fn().mockResolvedValue({ savedObjects: remappedAssets }); + sampleIntegration.deepCheck = jest.fn().mockResolvedValue({ ok: true, value: {} }); + sampleIntegration.getAssets = jest + .fn() + .mockResolvedValue({ ok: true, value: { savedObjects: remappedAssets } }); builder.postAssets = jest.fn().mockRejectedValue(new Error(errorMessage)); await expect(builder.build(sampleIntegration, options)).rejects.toThrowError(errorMessage); }); - - it('should reject with an error if getConfig returns null', async () => { - const options = { - dataSource: 'instance-datasource', - name: 'instance-name', - }; - sampleIntegration.getConfig = jest.fn().mockResolvedValue(null); - - await expect(builder.build(sampleIntegration, options)).rejects.toThrowError(); - }); }); describe('remapIDs', () => { @@ -264,8 +268,11 @@ describe('IntegrationInstanceBuilder', () => { it('should build an integration instance', async () => { const integration = { getConfig: jest.fn().mockResolvedValue({ - name: 'integration-template', - type: 'integration-type', + ok: true, + value: { + name: 'integration-template', + type: 'integration-type', + }, }), }; const refs = [ @@ -291,7 +298,7 @@ describe('IntegrationInstanceBuilder', () => { }; const instance = await builder.buildInstance( - (integration as unknown) as Integration, + (integration as unknown) as IntegrationReader, refs, options ); @@ -319,7 +326,7 @@ describe('IntegrationInstanceBuilder', () => { }; await expect( - builder.buildInstance((integration as unknown) as Integration, refs, options) + builder.buildInstance((integration as unknown) as IntegrationReader, refs, options) ).rejects.toThrowError(); }); }); diff --git a/server/adaptors/integrations/__test__/local_repository.test.ts b/server/adaptors/integrations/__test__/local_repository.test.ts index 6b0ddc72f..f1bfeb9b2 100644 --- a/server/adaptors/integrations/__test__/local_repository.test.ts +++ b/server/adaptors/integrations/__test__/local_repository.test.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Repository } from '../repository/repository'; -import { Integration } from '../repository/integration'; +import { RepositoryReader } from '../repository/repository'; +import { IntegrationReader } from '../repository/integration'; import path from 'path'; import * as fs from 'fs/promises'; @@ -20,15 +20,25 @@ describe('The local repository', () => { return Promise.resolve(null); } // Otherwise, all directories must be integrations - const integ = new Integration(integPath); - await expect(integ.check()).resolves.toBe(true); + const integ = new IntegrationReader(integPath); + expect(integ.getConfig()).resolves.toHaveProperty('ok', true); }) ); }); it('Should pass deep validation for all local integrations.', async () => { - const repository: Repository = new Repository(path.join(__dirname, '../__data__/repository')); - const integrations: Integration[] = await repository.getIntegrationList(); - await Promise.all(integrations.map((i) => expect(i.deepCheck()).resolves.toBeTruthy())); + const repository: RepositoryReader = new RepositoryReader( + path.join(__dirname, '../__data__/repository') + ); + const integrations: IntegrationReader[] = await repository.getIntegrationList(); + await Promise.all( + integrations.map(async (i) => { + const result = await i.deepCheck(); + if (!result.ok) { + console.error(result.error); + } + expect(result.ok).toBe(true); + }) + ); }); }); diff --git a/server/adaptors/integrations/__test__/kibana_backend.test.ts b/server/adaptors/integrations/__test__/manager.test.ts similarity index 70% rename from server/adaptors/integrations/__test__/kibana_backend.test.ts rename to server/adaptors/integrations/__test__/manager.test.ts index 63d62764c..75b89e520 100644 --- a/server/adaptors/integrations/__test__/kibana_backend.test.ts +++ b/server/adaptors/integrations/__test__/manager.test.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IntegrationsKibanaBackend } from '../integrations_kibana_backend'; +import { IntegrationsManager } from '../integrations_manager'; import { SavedObject, SavedObjectsClientContract } from '../../../../../../src/core/server/types'; -import { Repository } from '../repository/repository'; +import { RepositoryReader } from '../repository/repository'; import { IntegrationInstanceBuilder } from '../integrations_builder'; -import { Integration } from '../repository/integration'; +import { IntegrationReader } from '../repository/integration'; import { SavedObjectsFindResponse } from '../../../../../../src/core/server'; describe('IntegrationsKibanaBackend', () => { let mockSavedObjectsClient: jest.Mocked; - let mockRepository: jest.Mocked; - let backend: IntegrationsKibanaBackend; + let mockRepository: jest.Mocked; + let backend: IntegrationsManager; beforeEach(() => { mockSavedObjectsClient = { @@ -26,7 +26,7 @@ describe('IntegrationsKibanaBackend', () => { getIntegration: jest.fn(), getIntegrationList: jest.fn(), } as any; - backend = new IntegrationsKibanaBackend(mockSavedObjectsClient, mockRepository); + backend = new IntegrationsManager(mockSavedObjectsClient, mockRepository); }); describe('deleteIntegrationInstance', () => { @@ -147,24 +147,28 @@ describe('IntegrationsKibanaBackend', () => { describe('getIntegrationTemplates', () => { it('should get integration templates by name', async () => { const query = { name: 'template1' }; - const integration = { getConfig: jest.fn().mockResolvedValue({ name: 'template1' }) }; - mockRepository.getIntegration.mockResolvedValue((integration as unknown) as Integration); + const integration = { + getConfig: jest.fn().mockResolvedValue({ ok: true, value: { name: 'template1' } }), + }; + mockRepository.getIntegration.mockResolvedValue( + (integration as unknown) as IntegrationReader + ); const result = await backend.getIntegrationTemplates(query); expect(mockRepository.getIntegration).toHaveBeenCalledWith(query.name); expect(integration.getConfig).toHaveBeenCalled(); - expect(result).toEqual({ hits: [await integration.getConfig()] }); + expect(result).toEqual({ hits: [{ name: 'template1' }] }); }); it('should get all integration templates', async () => { const integrationList = [ - { getConfig: jest.fn().mockResolvedValue({ name: 'template1' }) }, - { getConfig: jest.fn().mockResolvedValue(null) }, - { getConfig: jest.fn().mockResolvedValue({ name: 'template2' }) }, + { getConfig: jest.fn().mockResolvedValue({ ok: true, value: { name: 'template1' } }) }, + { getConfig: jest.fn().mockResolvedValue({ ok: false, error: new Error() }) }, + { getConfig: jest.fn().mockResolvedValue({ ok: true, value: { name: 'template2' } }) }, ]; mockRepository.getIntegrationList.mockResolvedValue( - (integrationList as unknown) as Integration[] + (integrationList as unknown) as IntegrationReader[] ); const result = await backend.getIntegrationTemplates(); @@ -174,7 +178,7 @@ describe('IntegrationsKibanaBackend', () => { expect(integrationList[1].getConfig).toHaveBeenCalled(); expect(integrationList[2].getConfig).toHaveBeenCalled(); expect(result).toEqual({ - hits: [await integrationList[0].getConfig(), await integrationList[2].getConfig()], + hits: [{ name: 'template1' }, { name: 'template2' }], }); }); }); @@ -224,7 +228,7 @@ describe('IntegrationsKibanaBackend', () => { build: jest.fn().mockResolvedValue({ name, dataset: 'nginx', namespace: 'prod' }), }; const createdInstance = { name, dataset: 'nginx', namespace: 'prod' }; - mockRepository.getIntegration.mockResolvedValue((template as unknown) as Integration); + mockRepository.getIntegration.mockResolvedValue((template as unknown) as IntegrationReader); mockSavedObjectsClient.create.mockResolvedValue(({ result: 'created', } as unknown) as SavedObject); @@ -263,7 +267,7 @@ describe('IntegrationsKibanaBackend', () => { build: jest.fn().mockRejectedValue(new Error('Failed to build instance')), }; backend.instanceBuilder = (instanceBuilder as unknown) as IntegrationInstanceBuilder; - mockRepository.getIntegration.mockResolvedValue((template as unknown) as Integration); + mockRepository.getIntegration.mockResolvedValue((template as unknown) as IntegrationReader); await expect( backend.loadIntegrationInstance(templateName, name, 'datasource') @@ -277,9 +281,11 @@ describe('IntegrationsKibanaBackend', () => { const staticPath = 'path/to/static'; const assetData = Buffer.from('asset data'); const integration = { - getStatic: jest.fn().mockResolvedValue(assetData), + getStatic: jest.fn().mockResolvedValue({ ok: true, value: assetData }), }; - mockRepository.getIntegration.mockResolvedValue((integration as unknown) as Integration); + mockRepository.getIntegration.mockResolvedValue( + (integration as unknown) as IntegrationReader + ); const result = await backend.getStatic(templateName, staticPath); @@ -288,7 +294,7 @@ describe('IntegrationsKibanaBackend', () => { expect(result).toEqual(assetData); }); - it('should reject with a 404 if asset is not found', async () => { + it('should reject with a 404 if integration is not found', async () => { const templateName = 'template1'; const staticPath = 'path/to/static'; mockRepository.getIntegration.mockResolvedValue(null); @@ -298,6 +304,136 @@ describe('IntegrationsKibanaBackend', () => { 404 ); }); + + it('should reject with a 404 if static data is not found', async () => { + const templateName = 'template1'; + const staticPath = 'path/to/static'; + mockRepository.getIntegration.mockResolvedValue({ + getStatic: jest.fn().mockResolvedValue({ + ok: false, + error: { message: 'Not found', code: 'ENOENT' }, + }), + } as any); + + await expect(backend.getStatic(templateName, staticPath)).rejects.toHaveProperty( + 'statusCode', + 404 + ); + }); + }); + + describe('getSchemas', () => { + it('should get schema data', async () => { + const templateName = 'template1'; + const schemaData = { mappings: { test: {} } }; + const integration = { + getSchemas: jest.fn().mockResolvedValue({ ok: true, value: schemaData }), + }; + mockRepository.getIntegration.mockResolvedValue( + (integration as unknown) as IntegrationReader + ); + + const result = await backend.getSchemas(templateName); + + expect(mockRepository.getIntegration).toHaveBeenCalledWith(templateName); + expect(integration.getSchemas).toHaveBeenCalled(); + expect(result).toEqual(schemaData); + }); + + it('should reject with a 404 if integration is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue(null); + + await expect(backend.getSchemas(templateName)).rejects.toHaveProperty('statusCode', 404); + }); + + it('should reject with a 404 if schema data is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue({ + getSchemas: jest.fn().mockResolvedValue({ + ok: false, + error: { message: 'Not found', code: 'ENOENT' }, + }), + } as any); + + await expect(backend.getSchemas(templateName)).rejects.toHaveProperty('statusCode', 404); + }); + }); + + describe('getAssets', () => { + it('should get asset data', async () => { + const templateName = 'template1'; + const assetData = { savedObjects: [{ test: true }] }; + const integration = { + getAssets: jest.fn().mockResolvedValue({ ok: true, value: assetData }), + }; + mockRepository.getIntegration.mockResolvedValue( + (integration as unknown) as IntegrationReader + ); + + const result = await backend.getAssets(templateName); + + expect(mockRepository.getIntegration).toHaveBeenCalledWith(templateName); + expect(integration.getAssets).toHaveBeenCalled(); + expect(result).toEqual(assetData); + }); + + it('should reject with a 404 if integration is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue(null); + + await expect(backend.getAssets(templateName)).rejects.toHaveProperty('statusCode', 404); + }); + + it('should reject with a 404 if asset data is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue({ + getAssets: jest.fn().mockResolvedValue({ + ok: false, + error: { message: 'Not found', code: 'ENOENT' }, + }), + } as any); + + await expect(backend.getAssets(templateName)).rejects.toHaveProperty('statusCode', 404); + }); + }); + + describe('getSampleData', () => { + it('should get sample data', async () => { + const templateName = 'template1'; + const sampleData = { sampleData: [{ test: true }] }; + const integration = { + getSampleData: jest.fn().mockResolvedValue({ ok: true, value: sampleData }), + }; + mockRepository.getIntegration.mockResolvedValue( + (integration as unknown) as IntegrationReader + ); + + const result = await backend.getSampleData(templateName); + + expect(mockRepository.getIntegration).toHaveBeenCalledWith(templateName); + expect(integration.getSampleData).toHaveBeenCalled(); + expect(result).toEqual(sampleData); + }); + + it('should reject with a 404 if integration is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue(null); + + await expect(backend.getSampleData(templateName)).rejects.toHaveProperty('statusCode', 404); + }); + + it('should reject with a 404 if sample data is not found', async () => { + const templateName = 'template1'; + mockRepository.getIntegration.mockResolvedValue({ + getSampleData: jest.fn().mockResolvedValue({ + ok: false, + error: { message: 'Not found', code: 'ENOENT' }, + }), + } as any); + + await expect(backend.getSampleData(templateName)).rejects.toHaveProperty('statusCode', 404); + }); }); describe('getAssetStatus', () => { diff --git a/server/adaptors/integrations/__test__/validators.test.ts b/server/adaptors/integrations/__test__/validators.test.ts index ba573c4c4..6c09b595b 100644 --- a/server/adaptors/integrations/__test__/validators.test.ts +++ b/server/adaptors/integrations/__test__/validators.test.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { validateTemplate, validateInstance, ValidationResult } from '../validators'; +import { validateTemplate, validateInstance } from '../validators'; -const validTemplate: IntegrationTemplate = { +const validTemplate: IntegrationConfig = { name: 'test', version: '1.0.0', license: 'Apache-2.0', @@ -29,7 +29,7 @@ const validInstance: IntegrationInstance = { describe('validateTemplate', () => { it('Returns a success value for a valid Integration Template', () => { - const result: ValidationResult = validateTemplate(validTemplate); + const result: Result = validateTemplate(validTemplate); expect(result.ok).toBe(true); expect((result as any).value).toBe(validTemplate); }); @@ -38,7 +38,7 @@ describe('validateTemplate', () => { const sample: any = structuredClone(validTemplate); sample.license = undefined; - const result: ValidationResult = validateTemplate(sample); + const result: Result = validateTemplate(sample); expect(result.ok).toBe(false); expect((result as any).error).toBeInstanceOf(Error); @@ -48,7 +48,7 @@ describe('validateTemplate', () => { const sample: any = structuredClone(validTemplate); sample.components[0].name = 'not-logs'; - const result: ValidationResult = validateTemplate(sample); + const result: Result = validateTemplate(sample); expect(result.ok).toBe(false); expect((result as any).error).toBeInstanceOf(Error); @@ -62,7 +62,7 @@ describe('validateTemplate', () => { describe('validateInstance', () => { it('Returns true for a valid Integration Instance', () => { - const result: ValidationResult = validateInstance(validInstance); + const result: Result = validateInstance(validInstance); expect(result.ok).toBe(true); expect((result as any).value).toBe(validInstance); }); @@ -71,7 +71,7 @@ describe('validateInstance', () => { const sample: any = structuredClone(validInstance); sample.templateName = undefined; - const result: ValidationResult = validateInstance(sample); + const result: Result = validateInstance(sample); expect(result.ok).toBe(false); expect((result as any).error).toBeInstanceOf(Error); diff --git a/server/adaptors/integrations/integrations_adaptor.ts b/server/adaptors/integrations/integrations_adaptor.ts index cf7f4853e..574a4d25d 100644 --- a/server/adaptors/integrations/integrations_adaptor.ts +++ b/server/adaptors/integrations/integrations_adaptor.ts @@ -24,9 +24,7 @@ export interface IntegrationsAdaptor { getStatic: (templateName: string, path: string) => Promise; - getSchemas: ( - templateName: string - ) => Promise<{ mappings: { [key: string]: unknown }; schemas: { [key: string]: unknown } }>; + getSchemas: (templateName: string) => Promise<{ mappings: { [key: string]: unknown } }>; getAssets: (templateName: string) => Promise<{ savedObjects?: unknown }>; diff --git a/server/adaptors/integrations/integrations_builder.ts b/server/adaptors/integrations/integrations_builder.ts index b12e1a132..7a8026cea 100644 --- a/server/adaptors/integrations/integrations_builder.ts +++ b/server/adaptors/integrations/integrations_builder.ts @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { uuidRx } from 'public/components/custom_panels/redux/panel_slice'; import { SavedObjectsClientContract } from '../../../../../src/core/server'; -import { Integration } from './repository/integration'; +import { IntegrationReader } from './repository/integration'; import { SavedObjectsBulkCreateObject } from '../../../../../src/core/public'; interface BuilderOptions { @@ -21,15 +21,21 @@ export class IntegrationInstanceBuilder { this.client = client; } - async build(integration: Integration, options: BuilderOptions): Promise { + build(integration: IntegrationReader, options: BuilderOptions): Promise { const instance = integration .deepCheck() .then((result) => { - if (!result) { - return Promise.reject(new Error('Integration is not valid')); + if (!result.ok) { + return Promise.reject(result.error); } + return integration.getAssets(); + }) + .then((assets) => { + if (!assets.ok) { + return Promise.reject(assets.error); + } + return assets.value; }) - .then(() => integration.getAssets()) .then((assets) => this.remapIDs(assets.savedObjects!)) .then((assets) => this.remapDataSource(assets, options.dataSource)) .then((assets) => this.postAssets(assets)) @@ -86,14 +92,19 @@ export class IntegrationInstanceBuilder { } async buildInstance( - integration: Integration, + integration: IntegrationReader, refs: AssetReference[], options: BuilderOptions ): Promise { - const config: IntegrationTemplate = (await integration.getConfig())!; + const config: Result = await integration.getConfig(); + if (!config.ok) { + return Promise.reject( + new Error('Attempted to create instance with invalid template', config.error) + ); + } return Promise.resolve({ name: options.name, - templateName: config.name, + templateName: config.value.name, dataSource: options.dataSource, creationDate: new Date().toISOString(), assets: refs, diff --git a/server/adaptors/integrations/integrations_kibana_backend.ts b/server/adaptors/integrations/integrations_manager.ts similarity index 69% rename from server/adaptors/integrations/integrations_kibana_backend.ts rename to server/adaptors/integrations/integrations_manager.ts index f28c883ec..d365e48ee 100644 --- a/server/adaptors/integrations/integrations_kibana_backend.ts +++ b/server/adaptors/integrations/integrations_manager.ts @@ -4,20 +4,21 @@ */ import path from 'path'; -import { addRequestToMetric } from '../../../server/common/metrics/metrics_helper'; +import { addRequestToMetric } from '../../common/metrics/metrics_helper'; import { IntegrationsAdaptor } from './integrations_adaptor'; import { SavedObject, SavedObjectsClientContract } from '../../../../../src/core/server/types'; import { IntegrationInstanceBuilder } from './integrations_builder'; -import { Repository } from './repository/repository'; +import { RepositoryReader } from './repository/repository'; -export class IntegrationsKibanaBackend implements IntegrationsAdaptor { +export class IntegrationsManager implements IntegrationsAdaptor { client: SavedObjectsClientContract; instanceBuilder: IntegrationInstanceBuilder; - repository: Repository; + repository: RepositoryReader; - constructor(client: SavedObjectsClientContract, repository?: Repository) { + constructor(client: SavedObjectsClientContract, repository?: RepositoryReader) { this.client = client; - this.repository = repository ?? new Repository(path.join(__dirname, '__data__/repository')); + this.repository = + repository ?? new RepositoryReader(path.join(__dirname, '__data__/repository')); this.instanceBuilder = new IntegrationInstanceBuilder(this.client); } @@ -53,17 +54,33 @@ export class IntegrationsKibanaBackend implements IntegrationsAdaptor { return result; }; + // Internal; use getIntegrationTemplates. + _getAllIntegrationTemplates = async (): Promise => { + const integrationList = await this.repository.getIntegrationList(); + const configResults = await Promise.all(integrationList.map((x) => x.getConfig())); + const configs = configResults.filter((cfg) => cfg.ok) as Array<{ value: IntegrationConfig }>; + return Promise.resolve({ hits: configs.map((cfg) => cfg.value) }); + }; + + // Internal; use getIntegrationTemplates. + _getIntegrationTemplatesByName = async ( + name: string + ): Promise => { + const integration = await this.repository.getIntegration(name); + const config = await integration?.getConfig(); + if (!config || !config.ok) { + return Promise.resolve({ hits: [] }); + } + return Promise.resolve({ hits: [config.value] }); + }; + getIntegrationTemplates = async ( query?: IntegrationTemplateQuery ): Promise => { if (query?.name) { - const integration = await this.repository.getIntegration(query.name); - const config = await integration?.getConfig(); - return Promise.resolve({ hits: config ? [config] : [] }); + return this._getIntegrationTemplatesByName(query.name); } - const integrationList = await this.repository.getIntegrationList(); - const configList = await Promise.all(integrationList.map((x) => x.getConfig())); - return Promise.resolve({ hits: configList.filter((x) => x !== null) as IntegrationTemplate[] }); + return this._getAllIntegrationTemplates(); }; getIntegrationInstances = async ( @@ -159,17 +176,25 @@ export class IntegrationsKibanaBackend implements IntegrationsAdaptor { }; getStatic = async (templateName: string, staticPath: string): Promise => { - const data = await (await this.repository.getIntegration(templateName))?.getStatic(staticPath); - if (!data) { + const integration = await this.repository.getIntegration(templateName); + if (integration === null) { return Promise.reject({ - message: `Asset ${staticPath} not found`, + message: `Template ${templateName} not found`, statusCode: 404, }); } - return Promise.resolve(data); + const data = await integration.getStatic(staticPath); + if (data.ok) { + return data.value; + } + const is404 = (data.error as { code?: string }).code === 'ENOENT'; + return Promise.reject({ + message: data.error.message, + statusCode: is404 ? 404 : 500, + }); }; - getSchemas = async (templateName: string): Promise => { + getSchemas = async (templateName: string): Promise<{ mappings: { [key: string]: unknown } }> => { const integration = await this.repository.getIntegration(templateName); if (integration === null) { return Promise.reject({ @@ -177,7 +202,15 @@ export class IntegrationsKibanaBackend implements IntegrationsAdaptor { statusCode: 404, }); } - return Promise.resolve(integration.getSchemas()); + const result = await integration.getSchemas(); + if (result.ok) { + return result.value; + } + const is404 = (result.error as { code?: string }).code === 'ENOENT'; + return Promise.reject({ + message: result.error.message, + statusCode: is404 ? 404 : 500, + }); }; getAssets = async (templateName: string): Promise<{ savedObjects?: any }> => { @@ -188,7 +221,15 @@ export class IntegrationsKibanaBackend implements IntegrationsAdaptor { statusCode: 404, }); } - return Promise.resolve(integration.getAssets()); + const assets = await integration.getAssets(); + if (assets.ok) { + return assets.value; + } + const is404 = (assets.error as { code?: string }).code === 'ENOENT'; + return Promise.reject({ + message: assets.error.message, + statusCode: is404 ? 404 : 500, + }); }; getSampleData = async (templateName: string): Promise<{ sampleData: object[] | null }> => { @@ -199,6 +240,14 @@ export class IntegrationsKibanaBackend implements IntegrationsAdaptor { statusCode: 404, }); } - return Promise.resolve(integration.getSampleData()); + const sampleData = await integration.getSampleData(); + if (sampleData.ok) { + return sampleData.value; + } + const is404 = (sampleData.error as { code?: string }).code === 'ENOENT'; + return Promise.reject({ + message: sampleData.error.message, + statusCode: is404 ? 404 : 500, + }); }; } diff --git a/server/adaptors/integrations/repository/__test__/integration.test.ts b/server/adaptors/integrations/repository/__test__/integration.test.ts index 2002ad04a..ec77acac1 100644 --- a/server/adaptors/integrations/repository/__test__/integration.test.ts +++ b/server/adaptors/integrations/repository/__test__/integration.test.ts @@ -4,64 +4,19 @@ */ import * as fs from 'fs/promises'; -import { Integration } from '../integration'; +import { IntegrationReader } from '../integration'; import { Dirent, Stats } from 'fs'; import * as path from 'path'; +import { TEST_INTEGRATION_CONFIG } from '../../../../../test/constants'; jest.mock('fs/promises'); describe('Integration', () => { - let integration: Integration; - const sampleIntegration: IntegrationTemplate = { - name: 'sample', - version: '2.0.0', - license: 'Apache-2.0', - type: 'logs', - components: [ - { - name: 'logs', - version: '1.0.0', - }, - ], - assets: { - savedObjects: { - name: 'sample', - version: '1.0.1', - }, - }, - }; + let integration: IntegrationReader; beforeEach(() => { - integration = new Integration('./sample'); - }); - - describe('check', () => { - it('should return false if the directory does not exist', async () => { - const spy = jest.spyOn(fs, 'stat').mockResolvedValue({ isDirectory: () => false } as Stats); - - const result = await integration.check(); - - expect(spy).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('should return true if the directory exists and getConfig returns a valid template', async () => { - jest.spyOn(fs, 'stat').mockResolvedValue({ isDirectory: () => true } as Stats); - integration.getConfig = jest.fn().mockResolvedValue(sampleIntegration); - - const result = await integration.check(); - - expect(result).toBe(true); - }); - - it('should return false if the directory exists but getConfig returns null', async () => { - jest.spyOn(fs, 'stat').mockResolvedValue({ isDirectory: () => true } as Stats); - integration.getConfig = jest.fn().mockResolvedValue(null); - - const result = await integration.check(); - - expect(result).toBe(false); - }); + integration = new IntegrationReader('./sample'); + jest.spyOn(fs, 'lstat').mockResolvedValue({ isDirectory: () => true } as Stats); }); describe('getLatestVersion', () => { @@ -94,39 +49,46 @@ describe('Integration', () => { }); describe('getConfig', () => { + it('should return an error if the directory does not exist', async () => { + const spy = jest + .spyOn(fs, 'lstat') + .mockResolvedValueOnce({ isDirectory: () => false } as Stats); + + const result = await integration.getConfig(); + + expect(spy).toHaveBeenCalled(); + expect(result.ok).toBe(false); + }); + it('should return the parsed config template if it is valid', async () => { - jest.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify(sampleIntegration)); + jest.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify(TEST_INTEGRATION_CONFIG)); + jest.spyOn(fs, 'lstat').mockResolvedValueOnce({ isDirectory: () => true } as Stats); - const result = await integration.getConfig(sampleIntegration.version); + const result = await integration.getConfig(TEST_INTEGRATION_CONFIG.version); - expect(result).toEqual(sampleIntegration); + expect(result).toEqual({ ok: true, value: TEST_INTEGRATION_CONFIG }); }); - it('should return null and log validation errors if the config template is invalid', async () => { - const invalidTemplate = { ...sampleIntegration, version: 2 }; + it('should return an error if the config template is invalid', async () => { + const invalidTemplate = { ...TEST_INTEGRATION_CONFIG, version: 2 }; jest.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify(invalidTemplate)); - const logValidationErrorsMock = jest.spyOn(console, 'error'); - const result = await integration.getConfig(sampleIntegration.version); + const result = await integration.getConfig(TEST_INTEGRATION_CONFIG.version); - expect(result).toBeNull(); - expect(logValidationErrorsMock).toHaveBeenCalled(); + expect(result.ok).toBe(false); }); - it('should return null and log syntax errors if the config file has syntax errors', async () => { + it('should return an error if the config file has syntax errors', async () => { jest.spyOn(fs, 'readFile').mockResolvedValue('Invalid JSON'); - const logSyntaxErrorsMock = jest.spyOn(console, 'error'); - const result = await integration.getConfig(sampleIntegration.version); + const result = await integration.getConfig(TEST_INTEGRATION_CONFIG.version); - expect(result).toBeNull(); - expect(logSyntaxErrorsMock).toHaveBeenCalledWith(expect.any(String), expect.any(SyntaxError)); + expect(result.ok).toBe(false); }); - it('should return null and log errors if the integration config does not exist', async () => { - integration.directory = './non-existing-directory'; - const logErrorsMock = jest.spyOn(console, 'error'); - jest.spyOn(fs, 'readFile').mockImplementation((..._args) => { + it('should return an error if the integration config does not exist', async () => { + integration.directory = './empty-directory'; + const readFileMock = jest.spyOn(fs, 'readFile').mockImplementation((..._args) => { // Can't find any information on how to mock an actual file not found error, // But at least according to the current implementation this should be equivalent. const error: any = new Error('ENOENT: File not found'); @@ -134,39 +96,44 @@ describe('Integration', () => { return Promise.reject(error); }); - const result = await integration.getConfig(sampleIntegration.version); + const result = await integration.getConfig(TEST_INTEGRATION_CONFIG.version); - expect(jest.spyOn(fs, 'readFile')).toHaveBeenCalled(); - expect(logErrorsMock).toHaveBeenCalledWith(expect.any(String)); - expect(result).toBeNull(); + expect(readFileMock).toHaveBeenCalled(); + expect(result.ok).toBe(false); }); }); describe('getAssets', () => { it('should return linked saved object assets when available', async () => { - integration.getConfig = jest.fn().mockResolvedValue(sampleIntegration); + integration.getConfig = jest + .fn() + .mockResolvedValue({ ok: true, value: TEST_INTEGRATION_CONFIG }); jest.spyOn(fs, 'readFile').mockResolvedValue('{"name":"asset1"}\n{"name":"asset2"}'); - const result = await integration.getAssets(sampleIntegration.version); + const result = await integration.getAssets(TEST_INTEGRATION_CONFIG.version); - expect(result.savedObjects).toEqual([{ name: 'asset1' }, { name: 'asset2' }]); + expect(result.ok).toBe(true); + expect((result as any).value.savedObjects).toStrictEqual([ + { name: 'asset1' }, + { name: 'asset2' }, + ]); }); - it('should reject a return if the provided version has no config', async () => { - integration.getConfig = jest.fn().mockResolvedValue(null); + it('should return an error if the provided version has no config', async () => { + integration.getConfig = jest.fn().mockResolvedValue({ ok: false, error: new Error() }); - expect(integration.getAssets()).rejects.toThrowError(); + expect(integration.getAssets()).resolves.toHaveProperty('ok', false); }); - it('should log an error if the saved object assets are invalid', async () => { - const logErrorsMock = jest.spyOn(console, 'error'); - integration.getConfig = jest.fn().mockResolvedValue(sampleIntegration); + it('should return an error if the saved object assets are invalid', async () => { + integration.getConfig = jest + .fn() + .mockResolvedValue({ ok: true, value: TEST_INTEGRATION_CONFIG }); jest.spyOn(fs, 'readFile').mockResolvedValue('{"unclosed":'); - const result = await integration.getAssets(sampleIntegration.version); + const result = await integration.getAssets(TEST_INTEGRATION_CONFIG.version); - expect(logErrorsMock).toHaveBeenCalledWith(expect.any(String), expect.any(Error)); - expect(result.savedObjects).toBeUndefined(); + expect(result.ok).toBe(false); }); }); @@ -178,7 +145,7 @@ describe('Integration', () => { { name: 'component2', version: '2.0.0' }, ], }; - integration.getConfig = jest.fn().mockResolvedValue(sampleConfig); + integration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: sampleConfig }); const mappingFile1 = 'component1-1.0.0.mapping.json'; const mappingFile2 = 'component2-2.0.0.mapping.json'; @@ -190,7 +157,8 @@ describe('Integration', () => { const result = await integration.getSchemas(); - expect(result).toEqual({ + expect(result.ok).toBe(true); + expect((result as any).value).toStrictEqual({ mappings: { component1: { mapping: 'mapping1' }, component2: { mapping: 'mapping2' }, @@ -207,22 +175,20 @@ describe('Integration', () => { ); }); - it('should reject with an error if the config is null', async () => { - integration.getConfig = jest.fn().mockResolvedValue(null); + it('should reject with an error if the config is invalid', async () => { + integration.getConfig = jest.fn().mockResolvedValue({ ok: false, error: new Error() }); - await expect(integration.getSchemas()).rejects.toThrowError( - 'Attempted to get assets of invalid config' - ); + await expect(integration.getSchemas()).resolves.toHaveProperty('ok', false); }); it('should reject with an error if a mapping file is invalid', async () => { const sampleConfig = { components: [{ name: 'component1', version: '1.0.0' }], }; - integration.getConfig = jest.fn().mockResolvedValue(sampleConfig); + integration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: sampleConfig }); jest.spyOn(fs, 'readFile').mockRejectedValueOnce(new Error('Could not load schema')); - await expect(integration.getSchemas()).rejects.toThrowError('Could not load schema'); + await expect(integration.getSchemas()).resolves.toHaveProperty('ok', false); }); }); @@ -231,21 +197,56 @@ describe('Integration', () => { const readFileMock = jest .spyOn(fs, 'readFile') .mockResolvedValue(Buffer.from('logo data', 'ascii')); - expect(await integration.getStatic('/logo.png')).toStrictEqual( - Buffer.from('logo data', 'ascii') - ); + + const result = await integration.getStatic('logo.png'); + + expect(result.ok).toBe(true); + expect((result as any).value).toStrictEqual(Buffer.from('logo data', 'ascii')); expect(readFileMock).toBeCalledWith(path.join('sample', 'static', 'logo.png')); }); - it('should return null and log an error if the static file is not found', async () => { - const logErrorsMock = jest.spyOn(console, 'error'); + it('should return an error if the static file is not found', async () => { jest.spyOn(fs, 'readFile').mockImplementation((..._args) => { const error: any = new Error('ENOENT: File not found'); error.code = 'ENOENT'; return Promise.reject(error); }); - expect(await integration.getStatic('/logo.png')).toBeNull(); - expect(logErrorsMock).toBeCalledWith(expect.any(String)); + expect(integration.getStatic('/logo.png')).resolves.toHaveProperty('ok', false); + }); + }); + + describe('getSampleData', () => { + it('should return sample data', async () => { + const sampleConfig = { sampleData: { path: 'sample.json' } }; + integration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: sampleConfig }); + const readFileMock = jest.spyOn(fs, 'readFile').mockResolvedValue('[{"sample": true}]'); + + const result = await integration.getSampleData(); + + expect(result.ok).toBe(true); + expect((result as any).value.sampleData).toStrictEqual([{ sample: true }]); + expect(readFileMock).toBeCalledWith(path.join('sample', 'data', 'sample.json'), { + encoding: 'utf-8', + }); + }); + + it("should return null if there's no sample data", async () => { + integration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: {} }); + + const result = await integration.getSampleData(); + + expect(result.ok).toBe(true); + expect((result as any).value.sampleData).toBeNull(); + }); + + it('should catch and fail gracefully on invalid sample data', async () => { + const sampleConfig = { sampleData: { path: 'sample.json' } }; + integration.getConfig = jest.fn().mockResolvedValue({ ok: true, value: sampleConfig }); + jest.spyOn(fs, 'readFile').mockResolvedValue('[{"closingBracket": false]'); + + const result = await integration.getSampleData(); + + expect(result.ok).toBe(false); }); }); }); diff --git a/server/adaptors/integrations/repository/__test__/repository.test.ts b/server/adaptors/integrations/repository/__test__/repository.test.ts index 913968f49..d66fc5e86 100644 --- a/server/adaptors/integrations/repository/__test__/repository.test.ts +++ b/server/adaptors/integrations/repository/__test__/repository.test.ts @@ -4,36 +4,33 @@ */ import * as fs from 'fs/promises'; -import { Repository } from '../repository'; -import { Integration } from '../integration'; +import { RepositoryReader } from '../repository'; +import { IntegrationReader } from '../integration'; import { Dirent, Stats } from 'fs'; import path from 'path'; jest.mock('fs/promises'); describe('Repository', () => { - let repository: Repository; + let repository: RepositoryReader; beforeEach(() => { - repository = new Repository('path/to/directory'); + repository = new RepositoryReader('path/to/directory'); }); describe('getIntegrationList', () => { it('should return an array of Integration instances', async () => { - // Mock fs.readdir to return a list of folders jest.spyOn(fs, 'readdir').mockResolvedValue((['folder1', 'folder2'] as unknown) as Dirent[]); - - // Mock fs.lstat to return a directory status jest.spyOn(fs, 'lstat').mockResolvedValue({ isDirectory: () => true } as Stats); - - // Mock Integration check method to always return true - jest.spyOn(Integration.prototype, 'check').mockResolvedValue(true); + jest + .spyOn(IntegrationReader.prototype, 'getConfig') + .mockResolvedValue({ ok: true, value: {} as any }); const integrations = await repository.getIntegrationList(); expect(integrations).toHaveLength(2); - expect(integrations[0]).toBeInstanceOf(Integration); - expect(integrations[1]).toBeInstanceOf(Integration); + expect(integrations[0]).toBeInstanceOf(IntegrationReader); + expect(integrations[1]).toBeInstanceOf(IntegrationReader); }); it('should filter out null values from the integration list', async () => { @@ -41,19 +38,21 @@ describe('Repository', () => { // Mock fs.lstat to return a mix of directories and files jest.spyOn(fs, 'lstat').mockImplementation(async (toLstat) => { - if (toLstat === path.join('path', 'to', 'directory', 'folder1')) { + if (toLstat.toString().startsWith(path.join('path', 'to', 'directory', 'folder1'))) { return { isDirectory: () => true } as Stats; } else { return { isDirectory: () => false } as Stats; } }); - jest.spyOn(Integration.prototype, 'check').mockResolvedValue(true); + jest + .spyOn(IntegrationReader.prototype, 'getConfig') + .mockResolvedValue({ ok: true, value: {} as any }); const integrations = await repository.getIntegrationList(); expect(integrations).toHaveLength(1); - expect(integrations[0]).toBeInstanceOf(Integration); + expect(integrations[0]).toBeInstanceOf(IntegrationReader); }); it('should handle errors and return an empty array', async () => { @@ -67,15 +66,20 @@ describe('Repository', () => { describe('getIntegration', () => { it('should return an Integration instance if it exists and passes the check', async () => { - jest.spyOn(Integration.prototype, 'check').mockResolvedValue(true); + jest.spyOn(fs, 'lstat').mockResolvedValue({ isDirectory: () => true } as Stats); + jest + .spyOn(IntegrationReader.prototype, 'getConfig') + .mockResolvedValue({ ok: true, value: {} as any }); const integration = await repository.getIntegration('integrationName'); - expect(integration).toBeInstanceOf(Integration); + expect(integration).toBeInstanceOf(IntegrationReader); }); - it('should return null if the integration does not exist or fails the check', async () => { - jest.spyOn(Integration.prototype, 'check').mockResolvedValue(false); + it('should return null if the integration does not exist or fails checks', async () => { + jest + .spyOn(IntegrationReader.prototype, 'getConfig') + .mockResolvedValue({ ok: false, error: new Error() }); const integration = await repository.getIntegration('invalidIntegration'); diff --git a/server/adaptors/integrations/repository/catalog_data_adaptor.ts b/server/adaptors/integrations/repository/catalog_data_adaptor.ts new file mode 100644 index 000000000..6373fee4d --- /dev/null +++ b/server/adaptors/integrations/repository/catalog_data_adaptor.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +type IntegrationPart = 'assets' | 'data' | 'schemas' | 'static'; + +interface CatalogDataAdaptor { + /** + * Reads a Json or NDJson file from the data source. + * + * @param filename The name of the file to read. + * @param type Optional. The type of integration part to read ('assets', 'data', 'schemas', or 'static'). + * @returns A Promise that resolves with the content of the file as a string. + */ + readFile: (filename: string, type?: IntegrationPart) => Promise>; + + /** + * Reads a file from the data source as raw binary data. + * + * @param filename The name of the file to read. + * @param type Optional. The type of integration part to read ('assets', 'data', 'schemas', or 'static'). + * @returns A Promise that resolves with the content of the file as a Buffer. + */ + readFileRaw: (filename: string, type?: IntegrationPart) => Promise>; + + /** + * Reads the contents of a repository directory from the data source to find integrations. + * + * @param dirname The name of the directory to read. + * @returns A Promise that resolves with an array of filenames within the directory. + */ + findIntegrations: (dirname?: string) => Promise>; + + /** + * Reads the contents of an integration version to find available versions. + * + * @param dirname The name of the directory to read. + * @returns A Promise that resolves with an array of filenames within the directory. + */ + findIntegrationVersions: (dirname?: string) => Promise>; + + /** + * Determine whether a directory is an integration, repository, or otherwise. + * + * @param dirname The path to check. + * @returns A Promise that resolves with a boolean indicating whether the path is a directory or not. + */ + getDirectoryType: (dirname?: string) => Promise<'integration' | 'repository' | 'unknown'>; + + /** + * Creates a new CatalogDataAdaptor instance with the specified subdirectory appended to the current directory. + * Useful for exploring nested data without needing to know the instance type. + * + * @param subdirectory The path to append to the current directory. + * @returns A new CatalogDataAdaptor instance. + */ + join: (subdirectory: string) => CatalogDataAdaptor; +} diff --git a/server/adaptors/integrations/repository/fs_data_adaptor.ts b/server/adaptors/integrations/repository/fs_data_adaptor.ts new file mode 100644 index 000000000..df1c6781a --- /dev/null +++ b/server/adaptors/integrations/repository/fs_data_adaptor.ts @@ -0,0 +1,151 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs/promises'; +import path from 'path'; + +/** + * Helper function to compare version numbers. + * Assumes that the version numbers are valid, produces undefined behavior otherwise. + * + * @param a Left-hand number + * @param b Right-hand number + * @returns -1 if a > b, 1 if a < b, 0 otherwise. + */ +function compareVersions(a: string, b: string): number { + const aParts = a.split('.').map(Number.parseInt); + const bParts = b.split('.').map(Number.parseInt); + + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aValue = i < aParts.length ? aParts[i] : 0; + const bValue = i < bParts.length ? bParts[i] : 0; + + if (aValue > bValue) { + return -1; // a > b + } else if (aValue < bValue) { + return 1; // a < b + } + } + + return 0; // a == b +} + +function tryParseNDJson(content: string): object[] | null { + try { + const objects = []; + for (const line of content.split('\n')) { + if (line.trim() === '') { + // Other OSD ndjson parsers skip whitespace lines + continue; + } + objects.push(JSON.parse(line)); + } + return objects; + } catch (err: any) { + return null; + } +} + +/** + * A CatalogDataAdaptor that reads from the local filesystem. + * Used to read Integration information when the user uploads their own catalog. + */ +export class FileSystemCatalogDataAdaptor implements CatalogDataAdaptor { + directory: string; + + /** + * Creates a new FileSystemCatalogDataAdaptor instance. + * + * @param directory The base directory from which to read files. This is not sanitized. + */ + constructor(directory: string) { + this.directory = directory; + } + + async readFile(filename: string, type?: IntegrationPart): Promise> { + let content; + try { + content = await fs.readFile(path.join(this.directory, type ?? '.', filename), { + encoding: 'utf-8', + }); + } catch (err: any) { + return { ok: false, error: err }; + } + // First try to parse as JSON, then NDJSON, then fail. + try { + const parsed = JSON.parse(content); + return { ok: true, value: parsed }; + } catch (err: any) { + const parsed = tryParseNDJson(content); + if (parsed) { + return { ok: true, value: parsed }; + } + return { + ok: false, + error: new Error('Unable to parse file as JSON or NDJson', { cause: err }), + }; + } + } + + async readFileRaw(filename: string, type?: IntegrationPart): Promise> { + try { + const buffer = await fs.readFile(path.join(this.directory, type ?? '.', filename)); + return { ok: true, value: buffer }; + } catch (err: any) { + return { ok: false, error: err }; + } + } + + async findIntegrations(dirname: string = '.'): Promise> { + try { + const files = await fs.readdir(path.join(this.directory, dirname)); + return { ok: true, value: files }; + } catch (err: any) { + return { ok: false, error: err }; + } + } + + async findIntegrationVersions(dirname: string = '.'): Promise> { + let files; + const integPath = path.join(this.directory, dirname); + try { + files = await fs.readdir(integPath); + } catch (err: any) { + return { ok: false, error: err }; + } + const versions: string[] = []; + + for (const file of files) { + // TODO handle nested repositories -- assumes integrations are 1 level deep + if (path.extname(file) === '.json' && file.startsWith(`${path.basename(integPath)}-`)) { + const version = file.substring(path.basename(integPath).length + 1, file.length - 5); + if (!version.match(/^\d+(\.\d+)*$/)) { + continue; + } + versions.push(version); + } + } + + versions.sort((a, b) => compareVersions(a, b)); + return { ok: true, value: versions }; + } + + async getDirectoryType(dirname?: string): Promise<'integration' | 'repository' | 'unknown'> { + const isDir = (await fs.lstat(path.join(this.directory, dirname ?? '.'))).isDirectory(); + if (!isDir) { + return 'unknown'; + } + // Sloppily just check for one mandatory integration directory to distinguish. + // Improve if we need to distinguish a repository with an integration named "schemas". + const hasSchemas = ( + await fs.lstat(path.join(this.directory, dirname ?? '.', 'schemas')) + ).isDirectory(); + return hasSchemas ? 'integration' : 'repository'; + } + + join(filename: string): FileSystemCatalogDataAdaptor { + return new FileSystemCatalogDataAdaptor(path.join(this.directory, filename)); + } +} diff --git a/server/adaptors/integrations/repository/integration.ts b/server/adaptors/integrations/repository/integration.ts index 21f187bde..83e5779ac 100644 --- a/server/adaptors/integrations/repository/integration.ts +++ b/server/adaptors/integrations/repository/integration.ts @@ -3,107 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs/promises'; import path from 'path'; import { validateTemplate } from '../validators'; - -/** - * Helper function to compare version numbers. - * Assumes that the version numbers are valid, produces undefined behavior otherwise. - * - * @param a Left-hand number - * @param b Right-hand number - * @returns -1 if a > b, 1 if a < b, 0 otherwise. - */ -function compareVersions(a: string, b: string): number { - const aParts = a.split('.').map(Number.parseInt); - const bParts = b.split('.').map(Number.parseInt); - - for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { - const aValue = i < aParts.length ? aParts[i] : 0; - const bValue = i < bParts.length ? bParts[i] : 0; - - if (aValue > bValue) { - return -1; // a > b - } else if (aValue < bValue) { - return 1; // a < b - } - } - - return 0; // a == b -} - -/** - * Helper function to check if the given path is a directory - * - * @param dirPath The directory to check. - * @returns True if the path is a directory. - */ -async function isDirectory(dirPath: string): Promise { - try { - const stats = await fs.stat(dirPath); - return stats.isDirectory(); - } catch { - return false; - } -} +import { FileSystemCatalogDataAdaptor } from './fs_data_adaptor'; /** * The Integration class represents the data for Integration Templates. * It is backed by the repository file system. * It includes accessor methods for integration configs, as well as helpers for nested components. */ -export class Integration { +export class IntegrationReader { + reader: CatalogDataAdaptor; directory: string; name: string; - constructor(directory: string) { + constructor(directory: string, reader?: CatalogDataAdaptor) { this.directory = directory; this.name = path.basename(directory); + this.reader = reader ?? new FileSystemCatalogDataAdaptor(directory); } /** - * Check the integration for validity. - * This is not a deep check, but a quick check to verify that the integration is a valid directory and has a config file. + * Like getConfig(), but thoroughly checks all nested integration dependencies for validity. * - * @returns true if the integration is valid. + * @returns a Result indicating whether the integration is valid. */ - async check(): Promise { - if (!(await isDirectory(this.directory))) { - return false; - } - return (await this.getConfig()) !== null; - } - - /** - * Like check(), but thoroughly checks all nested integration dependencies. - * - * @returns true if the integration is valid. - */ - async deepCheck(): Promise { - if (!(await this.check())) { - console.error('check failed'); - return false; + async deepCheck(): Promise> { + const configResult = await this.getConfig(); + if (!configResult.ok) { + return configResult; } try { - // An integration must have at least one mapping const schemas = await this.getSchemas(); - if (Object.keys(schemas.mappings).length === 0) { - return false; + if (!schemas.ok || Object.keys(schemas.value.mappings).length === 0) { + return { ok: false, error: new Error('The integration has no schemas available') }; } - // An integration must have at least one asset const assets = await this.getAssets(); - if (Object.keys(assets).length === 0) { - return false; + if (!assets.ok || Object.keys(assets).length === 0) { + return { ok: false, error: new Error('An integration must have at least one asset') }; } } catch (err: any) { - // Any loading errors are considered invalid - console.error('Deep check failed for exception', err); - return false; + return { ok: false, error: err }; } - return true; + return configResult; } /** @@ -114,22 +58,12 @@ export class Integration { * @returns A string with the latest version, or null if no versions are available. */ async getLatestVersion(): Promise { - const files = await fs.readdir(this.directory); - const versions: string[] = []; - - for (const file of files) { - if (path.extname(file) === '.json' && file.startsWith(`${this.name}-`)) { - const version = file.substring(this.name.length + 1, file.length - 5); - if (!version.match(/^\d+(\.\d+)*$/)) { - continue; - } - versions.push(version); - } + const versions = await this.reader.findIntegrationVersions(); + if (!versions.ok) { + console.error(versions.error); + return null; } - - versions.sort((a, b) => compareVersions(a, b)); - - return versions.length > 0 ? versions[0] : null; + return versions.value.length > 0 ? versions.value[0] : null; } /** @@ -138,36 +72,27 @@ export class Integration { * @param version The version of the config to retrieve. * @returns The config if a valid config matching the version is present, otherwise null. */ - async getConfig(version?: string): Promise { + async getConfig(version?: string): Promise> { + if ((await this.reader.getDirectoryType()) !== 'integration') { + return { ok: false, error: new Error(`${this.directory} is not a valid integration`) }; + } + const maybeVersion: string | null = version ? version : await this.getLatestVersion(); if (maybeVersion === null) { - return null; + return { + ok: false, + error: new Error(`No valid config matching version ${version} is available`), + }; } const configFile = `${this.name}-${maybeVersion}.json`; - const configPath = path.join(this.directory, configFile); - try { - const config = await fs.readFile(configPath, { encoding: 'utf-8' }); - const possibleTemplate = JSON.parse(config); - const template = validateTemplate(possibleTemplate); - if (template.ok) { - return template.value; - } - console.error(template.error); - return null; - } catch (err: any) { - if (err instanceof SyntaxError) { - console.error(`Syntax errors in ${configFile}`, err); - return null; - } - if (err instanceof Error && (err as { code?: string }).code === 'ENOENT') { - console.error(`Attempted to retrieve non-existent config ${configFile}`); - return null; - } - throw new Error('Could not load integration', { cause: err }); + const config = await this.reader.readFile(configFile); + if (!config.ok) { + return config; } + return validateTemplate(config.value); } /** @@ -181,30 +106,59 @@ export class Integration { */ async getAssets( version?: string - ): Promise<{ - savedObjects?: object[]; - }> { - const config = await this.getConfig(version); - if (config === null) { - return Promise.reject(new Error('Attempted to get assets of invalid config')); + ): Promise< + Result<{ + savedObjects?: object[]; + queries?: Array<{ + query: string; + language: string; + }>; + }> + > { + const configResult = await this.getConfig(version); + if (!configResult.ok) { + return configResult; } - const result: { savedObjects?: object[] } = {}; + const config = configResult.value; + + const resultValue: { + savedObjects?: object[]; + queries?: Array<{ query: string; language: string }>; + } = {}; if (config.assets.savedObjects) { - const sobjPath = path.join( - this.directory, - 'assets', - `${config.assets.savedObjects.name}-${config.assets.savedObjects.version}.ndjson` + const sobjPath = `${config.assets.savedObjects.name}-${config.assets.savedObjects.version}.ndjson`; + const assets = await this.reader.readFile(sobjPath, 'assets'); + if (!assets.ok) { + return assets; + } + resultValue.savedObjects = assets.value as object[]; + } + if (config.assets.queries) { + resultValue.queries = []; + const queries = await Promise.all( + config.assets.queries.map(async (item) => { + const queryPath = `${item.name}-${item.version}.${item.language}`; + const query = await this.reader.readFileRaw(queryPath, 'assets'); + if (!query.ok) { + return query; + } + return { + ok: true as const, + value: { + language: item.language, + query: query.value.toString('utf8'), + }, + }; + }) ); - try { - const ndjson = await fs.readFile(sobjPath, { encoding: 'utf-8' }); - const asJson = '[' + ndjson.trim().replace(/\n/g, ',') + ']'; - const parsed = JSON.parse(asJson); - result.savedObjects = parsed; - } catch (err: any) { - console.error("Failed to load saved object assets, proceeding as if it's absent", err); + for (const query of queries) { + if (!query.ok) { + return query; + } + resultValue.queries.push(query.value); } } - return result; + return { ok: true, value: resultValue }; } /** @@ -217,38 +171,41 @@ export class Integration { */ async getSampleData( version?: string - ): Promise<{ - sampleData: object[] | null; - }> { - const config = await this.getConfig(version); - if (config === null) { - return Promise.reject(new Error('Attempted to get assets of invalid config')); + ): Promise< + Result<{ + sampleData: object[] | null; + }> + > { + const configResult = await this.getConfig(version); + if (!configResult.ok) { + return configResult; } - const result: { sampleData: object[] | null } = { sampleData: null }; + const config = configResult.value; + + const resultValue: { sampleData: object[] | null } = { sampleData: null }; if (config.sampleData) { - const sobjPath = path.join(this.directory, 'data', config.sampleData?.path); - try { - const jsonContent = await fs.readFile(sobjPath, { encoding: 'utf-8' }); - const parsed = JSON.parse(jsonContent) as object[]; - for (const value of parsed) { - if (!('@timestamp' in value)) { - continue; - } - // Randomly scatter timestamps across last 10 minutes - // Assume for now that the ordering of events isn't important, can change to a sequence if needed - // Also doesn't handle fields like `observedTimestamp` if present - Object.assign(value, { - '@timestamp': new Date( - Date.now() - Math.floor(Math.random() * 1000 * 60 * 10) - ).toISOString(), - }); + const jsonContent = await this.reader.readFile(config.sampleData.path, 'data'); + if (!jsonContent.ok) { + return jsonContent; + } + for (const value of jsonContent.value as object[]) { + if (!('@timestamp' in value)) { + continue; + } + // Randomly scatter timestamps across last 10 minutes + // Assume for now that the ordering of events isn't important, can change to a sequence if needed + // Also doesn't handle fields like `observedTimestamp` if present + const newTime = new Date( + Date.now() - Math.floor(Math.random() * 1000 * 60 * 10) + ).toISOString(); + Object.assign(value, { '@timestamp': newTime }); + if ('observedTimestamp' in value) { + Object.assign(value, { observedTimestamp: newTime }); } - result.sampleData = parsed; - } catch (err: any) { - console.error("Failed to load saved object assets, proceeding as if it's absent", err); } + resultValue.sampleData = jsonContent.value as object[]; } - return result; + return { ok: true, value: resultValue }; } /** @@ -263,32 +220,29 @@ export class Integration { */ async getSchemas( version?: string - ): Promise<{ - mappings: { [key: string]: any }; - }> { - const config = await this.getConfig(version); - if (config === null) { - return Promise.reject(new Error('Attempted to get assets of invalid config')); + ): Promise< + Result<{ + mappings: { [key: string]: any }; + }> + > { + const configResult = await this.getConfig(version); + if (!configResult.ok) { + return configResult; } - const result: { mappings: { [key: string]: any } } = { + const config = configResult.value; + + const resultValue: { mappings: { [key: string]: object } } = { mappings: {}, }; - try { - for (const component of config.components) { - const schemaFile = `${component.name}-${component.version}.mapping.json`; - const rawSchema = await fs.readFile(path.join(this.directory, 'schemas', schemaFile), { - encoding: 'utf-8', - }); - const parsedSchema = JSON.parse(rawSchema); - result.mappings[component.name] = parsedSchema; + for (const component of config.components) { + const schemaFile = `${component.name}-${component.version}.mapping.json`; + const schema = await this.reader.readFile(schemaFile, 'schemas'); + if (!schema.ok) { + return schema; } - } catch (err: any) { - // It's not clear that an invalid schema can be recovered from. - // For integrations to function, we need schemas to be valid. - console.error('Error loading schema', err); - return Promise.reject(new Error('Could not load schema', { cause: err })); + resultValue.mappings[component.name] = schema.value; } - return result; + return { ok: true, value: resultValue }; } /** @@ -297,16 +251,7 @@ export class Integration { * @param staticPath The path of the static to retrieve. * @returns A buffer with the static's data if present, otherwise null. */ - async getStatic(staticPath: string): Promise { - const fullStaticPath = path.join(this.directory, 'static', staticPath); - try { - return await fs.readFile(fullStaticPath); - } catch (err: any) { - if (err instanceof Error && (err as { code?: string }).code === 'ENOENT') { - console.error(`Static not found: ${staticPath}`); - return null; - } - throw err; - } + async getStatic(staticPath: string): Promise> { + return await this.reader.readFileRaw(staticPath, 'static'); } } diff --git a/server/adaptors/integrations/repository/repository.ts b/server/adaptors/integrations/repository/repository.ts index 00d241327..08200d474 100644 --- a/server/adaptors/integrations/repository/repository.ts +++ b/server/adaptors/integrations/repository/repository.ts @@ -3,39 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'fs/promises'; import * as path from 'path'; -import { Integration } from './integration'; +import { IntegrationReader } from './integration'; +import { FileSystemCatalogDataAdaptor } from './fs_data_adaptor'; -export class Repository { +export class RepositoryReader { + reader: CatalogDataAdaptor; directory: string; - constructor(directory: string) { + constructor(directory: string, reader?: CatalogDataAdaptor) { this.directory = directory; + this.reader = reader ?? new FileSystemCatalogDataAdaptor(directory); } - async getIntegrationList(): Promise { - try { - const folders = await fs.readdir(this.directory); - const integrations = Promise.all( - folders.map(async (folder) => { - const integPath = path.join(this.directory, folder); - if (!(await fs.lstat(integPath)).isDirectory()) { - return null; - } - const integ = new Integration(integPath); - return (await integ.check()) ? integ : null; - }) - ); - return (await integrations).filter((x) => x !== null) as Integration[]; - } catch (error) { - console.error(`Error reading integration directories in: ${this.directory}`, error); + async getIntegrationList(): Promise { + // TODO in the future, we want to support traversing nested directory structures. + const folders = await this.reader.findIntegrations(); + if (!folders.ok) { + console.error(`Error reading integration directories in: ${this.directory}`, folders.error); return []; } + const integrations = await Promise.all( + folders.value.map((i) => this.getIntegration(path.basename(i))) + ); + return integrations.filter((x) => x !== null) as IntegrationReader[]; } - async getIntegration(name: string): Promise { - const integ = new Integration(path.join(this.directory, name)); - return (await integ.check()) ? integ : null; + async getIntegration(name: string): Promise { + if ((await this.reader.getDirectoryType(name)) !== 'integration') { + console.error(`Requested integration '${name}' does not exist`); + return null; + } + const integ = new IntegrationReader(name, this.reader.join(name)); + const checkResult = await integ.getConfig(); + if (!checkResult.ok) { + console.error(`Integration '${name}' is invalid:`, checkResult.error); + return null; + } + return integ; } } diff --git a/server/adaptors/integrations/types.ts b/server/adaptors/integrations/types.ts index c12476909..fd5729afc 100644 --- a/server/adaptors/integrations/types.ts +++ b/server/adaptors/integrations/types.ts @@ -3,7 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -interface IntegrationTemplate { +type Result = { ok: true; value: T } | { ok: false; error: E }; + +interface IntegrationAssets { + savedObjects?: { + name: string; + version: string; + }; + queries?: Array<{ + name: string; + version: string; + language: string; + }>; +} + +interface IntegrationConfig { name: string; version: string; displayName?: string; @@ -20,12 +34,7 @@ interface IntegrationTemplate { darkModeGallery?: StaticAsset[]; }; components: IntegrationComponent[]; - assets: { - savedObjects?: { - name: string; - version: string; - }; - }; + assets: IntegrationAssets; sampleData?: { path: string; }; @@ -46,7 +55,7 @@ interface DisplayAsset { } interface IntegrationTemplateSearchResult { - hits: IntegrationTemplate[]; + hits: IntegrationConfig[]; } interface IntegrationTemplateQuery { diff --git a/server/adaptors/integrations/validators.ts b/server/adaptors/integrations/validators.ts index b40871eef..2a4a717a9 100644 --- a/server/adaptors/integrations/validators.ts +++ b/server/adaptors/integrations/validators.ts @@ -5,8 +5,6 @@ import Ajv, { JSONSchemaType } from 'ajv'; -export type ValidationResult = { ok: true; value: T } | { ok: false; error: E }; - const ajv = new Ajv(); const staticAsset: JSONSchemaType = { @@ -19,7 +17,7 @@ const staticAsset: JSONSchemaType = { additionalProperties: false, }; -const templateSchema: JSONSchemaType = { +const templateSchema: JSONSchemaType = { type: 'object', properties: { name: { type: 'string' }, @@ -66,6 +64,19 @@ const templateSchema: JSONSchemaType = { nullable: true, additionalProperties: false, }, + queries: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + version: { type: 'string' }, + language: { type: 'string' }, + }, + required: ['name', 'version', 'language'], + }, + nullable: true, + }, }, additionalProperties: false, }, @@ -118,11 +129,11 @@ const instanceValidator = ajv.compile(instanceSchema); * this is a more conventional wrapper that simplifies calling. * * @param data The data to be validated as an IntegrationTemplate. - * @return A ValidationResult indicating whether the validation was successful or not. + * @return A Result indicating whether the validation was successful or not. * If validation succeeds, returns an object with 'ok' set to true and the validated data. * If validation fails, returns an object with 'ok' set to false and an Error object describing the validation error. */ -export const validateTemplate = (data: unknown): ValidationResult => { +export const validateTemplate = (data: unknown): Result => { if (!templateValidator(data)) { return { ok: false, error: new Error(ajv.errorsText(templateValidator.errors)) }; } @@ -143,11 +154,11 @@ export const validateTemplate = (data: unknown): ValidationResult => { +export const validateInstance = (data: unknown): Result => { if (!instanceValidator(data)) { return { ok: false, diff --git a/server/adaptors/opensearch_observability_plugin.ts b/server/adaptors/opensearch_observability_plugin.ts index 0694d850c..012841fc4 100644 --- a/server/adaptors/opensearch_observability_plugin.ts +++ b/server/adaptors/opensearch_observability_plugin.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { JOBS_ENDPOINT_BASE, OPENSEARCH_PANELS_API } from '../../common/constants/shared'; +import { JOBS_ENDPOINT_BASE, OPENSEARCH_DATASOURCES_API, OPENSEARCH_PANELS_API } from '../../common/constants/shared'; export function OpenSearchObservabilityPlugin(Client: any, config: any, components: any) { const clientAction = components.clientAction.factory; diff --git a/server/adaptors/ppl_plugin.ts b/server/adaptors/ppl_plugin.ts index 304d196e3..6007f913f 100644 --- a/server/adaptors/ppl_plugin.ts +++ b/server/adaptors/ppl_plugin.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PPL_ENDPOINT, SQL_ENDPOINT } from '../../common/constants/shared'; +import { + OPENSEARCH_DATACONNECTIONS_API, + PPL_ENDPOINT, + SQL_ENDPOINT, +} from '../../common/constants/shared'; export const PPLPlugin = function (Client, config, components) { const ca = components.clientAction.factory; @@ -37,4 +41,53 @@ export const PPLPlugin = function (Client, config, components) { needBody: true, method: 'POST', }); + + ppl.getDataConnectionById = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}/<%=dataconnection%>`, + req: { + dataconnection: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + + ppl.deleteDataConnection = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}/<%=dataconnection%>`, + req: { + dataconnection: { + type: 'string', + required: true, + }, + }, + }, + method: 'DELETE', + }); + + ppl.createDataSource = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}`, + }, + needBody: true, + method: 'POST', + }); + + ppl.modifyDataConnection = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}`, + }, + needBody: true, + method: 'PUT', + }); + + ppl.getDataConnections = ca({ + url: { + fmt: `${OPENSEARCH_DATACONNECTIONS_API.DATACONNECTION}`, + }, + method: 'GET', + }); }; diff --git a/server/routes/data_connections/data_connections_router.ts b/server/routes/data_connections/data_connections_router.ts new file mode 100644 index 000000000..b75b53c43 --- /dev/null +++ b/server/routes/data_connections/data_connections_router.ts @@ -0,0 +1,173 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { + IOpenSearchDashboardsResponse, + IRouter, + ResponseError, +} from '../../../../../src/core/server'; +import { DATACONNECTIONS_BASE } from '../../../common/constants/shared'; + +export function registerDataConnectionsRoute(router: IRouter) { + router.get( + { + path: `${DATACONNECTIONS_BASE}/{name}`, + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response): Promise => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.getDataConnectionById', { + dataconnection: request.params.name, + }); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in fetching data connection:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + router.delete( + { + path: `${DATACONNECTIONS_BASE}/{name}`, + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response): Promise => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.deleteDataConnection', { + dataconnection: request.params.name, + }); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in deleting data connection:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + router.put( + { + path: `${DATACONNECTIONS_BASE}`, + validate: { + body: schema.object({ + name: schema.string(), + connector: schema.string(), + allowedRoles: schema.arrayOf(schema.string()), + properties: schema.any(), + }), + }, + }, + async (context, request, response): Promise => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.modifyDataConnection', { + body: { + name: request.body.name, + connector: request.body.connector, + allowedRoles: request.body.allowedRoles, + properties: request.body.properties, + }, + }); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in modifying data connection:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + router.post( + { + path: `${DATACONNECTIONS_BASE}`, + validate: { + body: schema.object({ + name: schema.string(), + connector: schema.string(), + allowedRoles: schema.arrayOf(schema.string()), + properties: schema.any(), + }), + }, + }, + async ( + context, + request, + response + ): Promise> => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.createDataSource', { + body: { + name: request.body.name, + connector: request.body.connector, + allowedRoles: request.body.allowedRoles, + properties: request.body.properties, + }, + }); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in creating data source:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.response, + }); + } + } + ); + + router.get( + { + path: `${DATACONNECTIONS_BASE}`, + validate: false, + }, + async (context, request, response): Promise => { + try { + const dataConnectionsresponse = await context.observability_plugin.observabilityClient + .asScoped(request) + .callAsCurrentUser('ppl.getDataConnections'); + return response.ok({ + body: dataConnectionsresponse, + }); + } catch (error: any) { + console.error('Issue in fetching data sources:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.response, + }); + } + } + ); +} diff --git a/server/routes/index.ts b/server/routes/index.ts index 4830bf58c..74c93884b 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -21,6 +21,7 @@ import { registerEventAnalyticsRouter } from './event_analytics/event_analytics_ import { registerAppAnalyticsRouter } from './application_analytics/app_analytics_router'; import { registerMetricsRoute } from './metrics/metrics_rounter'; import { registerIntegrationsRoute } from './integrations/integrations_router'; +import { registerDataConnectionsRoute } from './data_connections/data_connections_router'; export function setupRoutes({ router, client }: { router: IRouter; client: ILegacyClusterClient }) { PanelsRouter(router); @@ -42,4 +43,5 @@ export function setupRoutes({ router, client }: { router: IRouter; client: ILega registerMetricsRoute(router); registerIntegrationsRoute(router); + registerDataConnectionsRoute(router); } diff --git a/server/routes/integrations/integrations_router.ts b/server/routes/integrations/integrations_router.ts index 5a4813127..46fe47768 100644 --- a/server/routes/integrations/integrations_router.ts +++ b/server/routes/integrations/integrations_router.ts @@ -13,7 +13,7 @@ import { OpenSearchDashboardsRequest, OpenSearchDashboardsResponseFactory, } from '../../../../../src/core/server/http/router'; -import { IntegrationsKibanaBackend } from '../../adaptors/integrations/integrations_kibana_backend'; +import { IntegrationsManager } from '../../adaptors/integrations/integrations_manager'; /** * Handle an `OpenSearchDashboardsRequest` using the provided `callback` function. @@ -54,7 +54,7 @@ const getAdaptor = ( context: RequestHandlerContext, _request: OpenSearchDashboardsRequest ): IntegrationsAdaptor => { - return new IntegrationsKibanaBackend(context.core.savedObjects.client); + return new IntegrationsManager(context.core.savedObjects.client); }; export function registerIntegrationsRoute(router: IRouter) { diff --git a/test/constants.ts b/test/constants.ts index 6bacecdae..5d36c2514 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { IntegrationSetupInputs } from 'public/components/integrations/components/setup_integration'; + export const TEST_SPAN_RESPONSE = { took: 1, timed_out: false, @@ -557,3 +559,36 @@ export const TEST_SERVICE_MAP = { throughput: 16, }, }; + +export const TEST_INTEGRATION_SETUP_INPUTS: IntegrationSetupInputs = { + displayName: 'Test Instance Name', + connectionType: 'index', + connectionDataSource: 'ss4o_logs-nginx-test', +}; + +// TODO fill in the rest of the fields +export const TEST_INTEGRATION_CONFIG: IntegrationConfig = { + name: 'sample', + version: '2.0.0', + license: 'Apache-2.0', + type: 'logs', + components: [ + { + name: 'logs', + version: '1.0.0', + }, + ], + assets: { + savedObjects: { + name: 'sample', + version: '1.0.1', + }, + }, +}; + +export const mockSavedObjectActions = ({ get = [], getBulk = [] }) => { + return { + get: jest.fn().mockResolvedValue({ observabilityObjectList: get }), + getBulk: jest.fn().mockResolvedValue({ observabilityObjectList: getBulk }), + }; +}; diff --git a/test/datasources.ts b/test/datasources.ts new file mode 100644 index 000000000..6258101fc --- /dev/null +++ b/test/datasources.ts @@ -0,0 +1,808 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatasourceType } from '../common/types/data_connections'; + +export const showDataConnectionsData = { + schema: [ + { + name: 'DATASOURCE_NAME', + type: 'string', + }, + { + name: 'CONNECTOR_TYPE', + type: 'string', + }, + ], + datarows: [ + ['my_spark_actual', 'SPARK'], + ['@opensearch', 'OPENSEARCH'], + ['my_spark', 'SPARK'], + ], + total: 3, + size: 3, + jsonData: [ + { + DATASOURCE_NAME: 'my_spark3', + CONNECTOR_TYPE: 'SPARK', + }, + { + DATASOURCE_NAME: 'my_spark4', + CONNECTOR_TYPE: 'SPARK', + }, + { + DATASOURCE_NAME: 'my_spark', + CONNECTOR_TYPE: 'SPARK', + }, + { + DATASOURCE_NAME: 'my_spark2', + CONNECTOR_TYPE: 'SPARK', + }, + ], +}; + +export const describePrometheusDataConnection = { + name: 'prom', + description: '', + connector: 'PROMETHEUS', + allowedRoles: [], + properties: { + 'prometheus.uri': 'localhost:9201', + }, +}; + +export const testS3ConnectionDetails = { + dataConnection: 'ya', + description: '', + connector: 'S3GLUE' as DatasourceType, + properties: { + 'glue.indexstore.opensearch.uri': 'y', + 'glue.indexstore.opensearch.region': 'us-west-2', + }, +}; + +export const testPrometheusConnectionDetails = { + dataConnection: 'prom', + description: '', + connector: 'PROMETHEUS' as DatasourceType, + properties: { + 'prometheus.uri': 'localhost:9201', + }, +}; + +export const describeS3Dataconnection = { + name: 'ya', + description: '', + connector: 'S3GLUE', + allowedRoles: [], + properties: { + 'glue.indexstore.opensearch.uri': 'y', + 'glue.indexstore.opensearch.region': 'us-west-2', + }, +}; + +export const mockRoleData = { + total: 44, + data: { + security_analytics_ack_alerts: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opensearch/securityanalytics/alerts/*'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + observability_read_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opensearch/observability/get'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + kibana_user: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for a kibana user', + cluster_permissions: ['cluster_composite_ops'], + index_permissions: [ + { + index_patterns: [ + '.kibana', + '.kibana-6', + '.kibana_*', + '.opensearch_dashboards', + '.opensearch_dashboards-6', + '.opensearch_dashboards_*', + ], + fls: [], + masked_fields: [], + allowed_actions: ['delete', 'index', 'manage', 'read'], + }, + { + index_patterns: ['.tasks', '.management-beats', '*:.tasks', '*:.management-beats'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + ], + tenant_permissions: [], + static: true, + }, + own_index: { + reserved: true, + hidden: false, + description: 'Allow all for indices named like the current user', + cluster_permissions: ['cluster_composite_ops'], + index_permissions: [ + { + index_patterns: ['${user_name}'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + ], + tenant_permissions: [], + static: true, + }, + alerting_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/alerting/*', + 'cluster:admin/opensearch/alerting/*', + 'cluster:admin/opensearch/notifications/feature/publish', + 'cluster_monitor', + ], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: [ + 'indices:admin/aliases/get', + 'indices:admin/mappings/get', + 'indices_monitor', + ], + }, + ], + tenant_permissions: [], + static: false, + }, + snapshot_management_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/snapshot_management/policy/explain', + 'cluster:admin/opensearch/snapshot_management/policy/get', + 'cluster:admin/opensearch/snapshot_management/policy/search', + 'cluster:admin/repository/get', + 'cluster:admin/snapshot/get', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + all_access: { + reserved: true, + hidden: false, + description: 'Allow full access to all indices and all cluster APIs', + cluster_permissions: ['*'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['*'], + }, + ], + tenant_permissions: [ + { + tenant_patterns: ['*'], + allowed_actions: ['kibana_all_write'], + }, + ], + static: true, + }, + alerting_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/alerting/alerts/get', + 'cluster:admin/opendistro/alerting/destination/get', + 'cluster:admin/opendistro/alerting/monitor/get', + 'cluster:admin/opendistro/alerting/monitor/search', + 'cluster:admin/opensearch/alerting/findings/get', + 'cluster:admin/opensearch/alerting/workflow/get', + 'cluster:admin/opensearch/alerting/workflow_alerts/get', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + cross_cluster_replication_follower_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/plugins/replication/autofollow/update'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: [ + 'indices:admin/plugins/replication/index/pause', + 'indices:admin/plugins/replication/index/resume', + 'indices:admin/plugins/replication/index/setup/validate', + 'indices:admin/plugins/replication/index/start', + 'indices:admin/plugins/replication/index/status_check', + 'indices:admin/plugins/replication/index/stop', + 'indices:admin/plugins/replication/index/update', + 'indices:data/write/plugins/replication/changes', + ], + }, + ], + tenant_permissions: [], + static: false, + }, + manage_snapshots: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for managing snapshots', + cluster_permissions: ['manage_snapshots'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:admin/create', 'indices:data/write/index'], + }, + ], + tenant_permissions: [], + static: true, + }, + logstash: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for logstash and beats', + cluster_permissions: [ + 'cluster:admin/ingest/pipeline/get', + 'cluster:admin/ingest/pipeline/put', + 'cluster_composite_ops', + 'cluster_monitor', + 'indices:admin/template/get', + 'indices:admin/template/put', + ], + index_permissions: [ + { + index_patterns: ['logstash-*'], + fls: [], + masked_fields: [], + allowed_actions: ['create_index', 'crud'], + }, + { + index_patterns: ['*beat*'], + fls: [], + masked_fields: [], + allowed_actions: ['crud', 'create_index'], + }, + ], + tenant_permissions: [], + static: true, + }, + observability_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/observability/create', + 'cluster:admin/opensearch/observability/delete', + 'cluster:admin/opensearch/observability/get', + 'cluster:admin/opensearch/observability/update', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + point_in_time_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['manage_point_in_time'], + }, + ], + tenant_permissions: [], + static: false, + }, + notifications_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opensearch/notifications/*'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + notifications_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/notifications/channels/get', + 'cluster:admin/opensearch/notifications/configs/get', + 'cluster:admin/opensearch/notifications/features', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + cross_cluster_replication_leader_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: [ + 'indices:admin/plugins/replication/index/setup/validate', + 'indices:data/read/plugins/replication/changes', + 'indices:data/read/plugins/replication/file_chunk', + ], + }, + ], + tenant_permissions: [], + static: false, + }, + knn_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/knn_get_model_action', + 'cluster:admin/knn_search_model_action', + 'cluster:admin/knn_stats_action', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + ppl_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opensearch/ppl'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: [ + 'indices:admin/mappings/get', + 'indices:data/read/search*', + 'indices:monitor/settings/get', + ], + }, + ], + tenant_permissions: [], + static: false, + }, + security_analytics_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/securityanalytics/alerts/get', + 'cluster:admin/opensearch/securityanalytics/correlations/findings', + 'cluster:admin/opensearch/securityanalytics/correlations/list', + 'cluster:admin/opensearch/securityanalytics/detector/get', + 'cluster:admin/opensearch/securityanalytics/detector/search', + 'cluster:admin/opensearch/securityanalytics/findings/get', + 'cluster:admin/opensearch/securityanalytics/mapping/get', + 'cluster:admin/opensearch/securityanalytics/mapping/view/get', + 'cluster:admin/opensearch/securityanalytics/rule/get', + 'cluster:admin/opensearch/securityanalytics/rule/search', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + security_analytics_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/securityanalytics/alerts/*', + 'cluster:admin/opensearch/securityanalytics/correlations/*', + 'cluster:admin/opensearch/securityanalytics/detector/*', + 'cluster:admin/opensearch/securityanalytics/findings/*', + 'cluster:admin/opensearch/securityanalytics/mapping/*', + 'cluster:admin/opensearch/securityanalytics/rule/*', + ], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:admin/mapping/put', 'indices:admin/mappings/get'], + }, + ], + tenant_permissions: [], + static: false, + }, + knn_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/knn_delete_model_action', + 'cluster:admin/knn_get_model_action', + 'cluster:admin/knn_remove_model_from_cache_action', + 'cluster:admin/knn_search_model_action', + 'cluster:admin/knn_stats_action', + 'cluster:admin/knn_training_job_route_decision_info_action', + 'cluster:admin/knn_training_job_router_action', + 'cluster:admin/knn_training_model_action', + 'cluster:admin/knn_update_model_graveyard_action', + 'cluster:admin/knn_warmup_action', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + asynchronous_search_read_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opendistro/asynchronous_search/get'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + index_management_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/ism/*', + 'cluster:admin/opendistro/rollup/*', + 'cluster:admin/opendistro/transform/*', + 'cluster:admin/opensearch/controlcenter/lron/*', + 'cluster:admin/opensearch/notifications/channels/get', + 'cluster:admin/opensearch/notifications/feature/publish', + ], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:admin/opensearch/ism/*'], + }, + ], + tenant_permissions: [], + static: false, + }, + readall_and_monitor: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for to readall indices and monitor the cluster', + cluster_permissions: ['cluster_composite_ops_ro', 'cluster_monitor'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['read'], + }, + ], + tenant_permissions: [], + static: true, + }, + ml_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/ml/connectors/get', + 'cluster:admin/opensearch/ml/connectors/search', + 'cluster:admin/opensearch/ml/model_groups/search', + 'cluster:admin/opensearch/ml/models/get', + 'cluster:admin/opensearch/ml/models/search', + 'cluster:admin/opensearch/ml/tasks/get', + 'cluster:admin/opensearch/ml/tasks/search', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + security_rest_api_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'restapi:admin/actiongroups', + 'restapi:admin/allowlist', + 'restapi:admin/internalusers', + 'restapi:admin/nodesdn', + 'restapi:admin/roles', + 'restapi:admin/rolesmapping', + 'restapi:admin/ssl/certs/info', + 'restapi:admin/ssl/certs/reload', + 'restapi:admin/tenants', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + kibana_read_only: { + reserved: true, + hidden: false, + cluster_permissions: [], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + cross_cluster_search_remote_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:admin/shards/search_shards', 'indices:data/read/search'], + }, + ], + tenant_permissions: [], + static: false, + }, + reports_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/reports/definition/get', + 'cluster:admin/opendistro/reports/definition/list', + 'cluster:admin/opendistro/reports/instance/get', + 'cluster:admin/opendistro/reports/instance/list', + 'cluster:admin/opendistro/reports/menu/download', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + anomaly_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/ad/detector/info', + 'cluster:admin/opendistro/ad/detector/search', + 'cluster:admin/opendistro/ad/detector/validate', + 'cluster:admin/opendistro/ad/detectors/get', + 'cluster:admin/opendistro/ad/result/search', + 'cluster:admin/opendistro/ad/result/topAnomalies', + 'cluster:admin/opendistro/ad/tasks/search', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + anomaly_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opendistro/ad/*', 'cluster_monitor'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: [ + 'indices:admin/aliases/get', + 'indices:admin/mappings/get', + 'indices_monitor', + ], + }, + ], + tenant_permissions: [], + static: false, + }, + reports_instances_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/reports/instance/get', + 'cluster:admin/opendistro/reports/instance/list', + 'cluster:admin/opendistro/reports/menu/download', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + snapshot_management_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opensearch/notifications/feature/publish', + 'cluster:admin/opensearch/snapshot_management/*', + 'cluster:admin/repository/*', + 'cluster:admin/snapshot/*', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + readall: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for to readall indices', + cluster_permissions: ['cluster_composite_ops_ro'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['read'], + }, + ], + tenant_permissions: [], + static: true, + }, + asynchronous_search_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opendistro/asynchronous_search/*'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:data/read/search*'], + }, + ], + tenant_permissions: [], + static: false, + }, + ml_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/opensearch/ml/*', 'cluster_monitor'], + index_permissions: [ + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_monitor'], + }, + ], + tenant_permissions: [], + static: false, + }, + reports_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/reports/definition/create', + 'cluster:admin/opendistro/reports/definition/delete', + 'cluster:admin/opendistro/reports/definition/get', + 'cluster:admin/opendistro/reports/definition/list', + 'cluster:admin/opendistro/reports/definition/on_demand', + 'cluster:admin/opendistro/reports/definition/update', + 'cluster:admin/opendistro/reports/instance/get', + 'cluster:admin/opendistro/reports/instance/list', + 'cluster:admin/opendistro/reports/menu/download', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + security_rest_api_access: { + reserved: true, + hidden: false, + cluster_permissions: [], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + ip2geo_datasource_read_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/geospatial/datasource/get'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + alerting_ack_alerts: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/alerting/alerts/*', + 'cluster:admin/opendistro/alerting/chained_alerts/*', + 'cluster:admin/opendistro/alerting/workflow_alerts/*', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + ip2geo_datasource_full_access: { + reserved: true, + hidden: false, + cluster_permissions: ['cluster:admin/geospatial/datasource/*'], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + kibana_server: { + reserved: true, + hidden: false, + description: 'Provide the minimum permissions for the Kibana server', + cluster_permissions: [ + 'cluster_composite_ops', + 'cluster_monitor', + 'indices:admin/index_template*', + 'indices:admin/template*', + 'indices:data/read/scroll*', + 'manage_point_in_time', + ], + index_permissions: [ + { + index_patterns: ['.kibana', '.opensearch_dashboards'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + { + index_patterns: ['.kibana-6', '.opensearch_dashboards-6'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + { + index_patterns: ['.kibana_*', '.opensearch_dashboards_*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + { + index_patterns: ['.tasks'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + { + index_patterns: ['.management-beats*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices_all'], + }, + { + index_patterns: ['*'], + fls: [], + masked_fields: [], + allowed_actions: ['indices:admin/aliases*'], + }, + ], + tenant_permissions: [], + static: true, + }, + notebooks_read_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/notebooks/get', + 'cluster:admin/opendistro/notebooks/list', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + notebooks_full_access: { + reserved: true, + hidden: false, + cluster_permissions: [ + 'cluster:admin/opendistro/notebooks/create', + 'cluster:admin/opendistro/notebooks/delete', + 'cluster:admin/opendistro/notebooks/get', + 'cluster:admin/opendistro/notebooks/list', + 'cluster:admin/opendistro/notebooks/update', + ], + index_permissions: [], + tenant_permissions: [], + static: false, + }, + }, +}; diff --git a/test/event_analytics_constants.ts b/test/event_analytics_constants.ts index cf8df8a0e..0adbd5b65 100644 --- a/test/event_analytics_constants.ts +++ b/test/event_analytics_constants.ts @@ -567,3 +567,10 @@ export const HORIZONTAL_BAR_TEST_VISUALIZATIONS_DATA = { type: VIS_CHART_TYPES.HorizontalBar, }), }; + +export const EXPLORER_START_TIME = 'Aug 28, 2023 @ 20:00:00.406'; + +export const EXPLORER_END_TIME = 'Aug 28, 2023 @ 20:00:00.408'; + +export const EXPLORER_DATA_GRID_QUERY = + "source = opensearch_dashboards_sample_data_logs | where match(request,'filebeat')"; diff --git a/yarn.lock b/yarn.lock index c32ae0453..b8455da08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,29 +3,29 @@ "@algolia/autocomplete-core@^1.4.1": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.11.0.tgz#9db68f2aa38fe3149507d214082a1926b6b91fac" - integrity sha512-kFtn8XPMdE1QGDxyMTObGgaUpq5lcG2fLVsda6E88MoZZsfYkC8Oua6dwa0b06/GpgEWaliby/7AksUqz05uzw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.11.1.tgz#73dce6a65430a872cd9474babf052dcb3ca1d6fe" + integrity sha512-C4ZaUbwNHOkbXM+vsUpx9AYhfLRCcku4tjn64Dr6/WjBhD1gv/WcI/GlvTc7QU53xPubNm8pfnfFAjRogEdnNQ== dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.11.0" - "@algolia/autocomplete-shared" "1.11.0" + "@algolia/autocomplete-plugin-algolia-insights" "1.11.1" + "@algolia/autocomplete-shared" "1.11.1" -"@algolia/autocomplete-plugin-algolia-insights@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.11.0.tgz#edae2ebe5d88afe62ce3efa1723289e5ae376ce3" - integrity sha512-TsJ5vs1jR9IbYDRWnd0tHLF/y54quoSAV7fDbyDdfUdkuI9bVP0bzulxT+POezPT5+6Ya5IJNCrg4DViA3Dm0Q== +"@algolia/autocomplete-plugin-algolia-insights@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.11.1.tgz#46e7d7c75a10e33ca6310ecc920a19a2fed13570" + integrity sha512-Ajaav4irJrbwLuQ0hYuaZlUH1pY7iobXSFfQsHFSQ+m2Q8r/h1GtkaiRCpcfnwO8CURdcD3RFMc0pClOPzmJeA== dependencies: - "@algolia/autocomplete-shared" "1.11.0" + "@algolia/autocomplete-shared" "1.11.1" -"@algolia/autocomplete-shared@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.11.0.tgz#2ba14b056e695ad2bbd0eb04f5d6dcbd3584c751" - integrity sha512-ug1HYGQfe8+bvGuVJ3Fbdxn+YvR6MHPD36vQ5kv+5WWnBiW+QTyGk5yiluS9+i81l9wxH34Zl3XN/6MQ68MAgw== +"@algolia/autocomplete-shared@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.11.1.tgz#ce5efbc94376954ea78848b0d45b3254e91ce259" + integrity sha512-bbX7dk41aAy7jlgaJTH/Suv7moGvmkudrrF2ECuMQUrWvl/xGfrj9ZYpLcMsT7TcTYf5SPtK5awXJnpQ4PTKEg== "@algolia/autocomplete-theme-classic@^1.2.1": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.11.0.tgz#3523744fc244f1979850560a898f5c562664ee08" - integrity sha512-R6k8D/6rwI5EQliVweK+JvX6JAF2cnzJvWhfgwOkdkVHYX3RT9yXR8aE7m6Rxv8wtQpivGsCKeTEJl2jD5goEw== + version "1.11.1" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-theme-classic/-/autocomplete-theme-classic-1.11.1.tgz#78895aa6551cc4d02df3b4f68899b41fb941027f" + integrity sha512-AsKpXXpxIjxOjPNuxWNI7gcbxebxkb18AV36qH6CO6LSAkxZ7SFwEcHwtOlCtk0lGfWZxKWJwI4jiclucBYYIA== "@babel/code-frame@^7.0.0": version "7.22.13" @@ -35,24 +35,24 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/helper-validator-identifier@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044" - integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== "@babel/highlight@^7.22.13": - version "7.22.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" - integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== dependencies: - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" chalk "^2.4.2" js-tokens "^4.0.0" "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.9.2": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" - integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== dependencies: regenerator-runtime "^0.14.0" @@ -216,9 +216,9 @@ react-toggle "^4.1.1" "@reduxjs/toolkit@^1.6.1": - version "1.9.5" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" - integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ== + version "1.9.7" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.7.tgz#7fc07c0b0ebec52043f8cb43510cf346405f78a6" + integrity sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ== dependencies: immer "^9.0.21" redux "^4.2.1" @@ -243,24 +243,24 @@ integrity sha512-Rt4IC1T7xkCWa0OG1oSsPa0iqnxlDeQqKXZAHrQGLb7wFGncWm85MaxKUjAGejOrUynOgWlFi4c6S6IyJwoK4g== "@types/enzyme-adapter-react-16@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz#8aca7ae2fd6c7137d869b6616e696d21bb8b0cec" - integrity sha512-VonDkZ15jzqDWL8mPFIQnnLtjwebuL9YnDkqeCDYnB4IVgwUm0mwKkqhrxLL6mb05xm7qqa3IE95m8CZE9imCg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.7.tgz#88e94608581c0a1a66f82b5c1ff641a09bcfa22b" + integrity sha512-BQLgUf3vbuzi37iCjhXNG/iBmCzuLxlIxhkafO8AJisXxRbGOfbuJi1Stwlwr5oj/6/Ea7mViKG7Wh54n0ULXw== dependencies: "@types/enzyme" "*" "@types/enzyme@*": - version "3.10.13" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.13.tgz#332c0ed59b01f7b1c398c532a1c21a5feefabeb1" - integrity sha512-FCtoUhmFsud0Yx9fmZk179GkdZ4U9B0GFte64/Md+W/agx0L5SxsIIbhLBOxIb9y2UfBA4WQnaG1Od/UsUQs9Q== + version "3.10.14" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.14.tgz#dc30e3820a6331e8165be442cc778f25457ec44d" + integrity sha512-JeTG2MNUX1bH2DqccwUe3SuPoLu+kUz5UgR3Tvl9nBdfNj7rBZscytctSjEatd5Ul9GXXGKaQBaxODIgJYVRqA== dependencies: "@types/cheerio" "*" "@types/react" "^16" "@types/hast@^2.0.0": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.5.tgz#08caac88b44d0fdd04dc17a19142355f43bd8a7a" - integrity sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg== + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.6.tgz#bb8b05602112a26d22868acb70c4b20984ec7086" + integrity sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg== dependencies: "@types/unist" "^2" @@ -270,72 +270,72 @@ integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#412e0725ef41cde73bfa03e0e833eaff41e0fd63" + integrity sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz#edc8e421991a3b4df875036d381fc0a5a982f549" + integrity sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A== dependencies: "@types/istanbul-lib-report" "*" "@types/mime@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.2.tgz#c1ae807f13d308ee7511a5b81c74f327028e66e8" + integrity sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ== "@types/node@*": - version "20.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" - integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + version "20.8.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" + integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== "@types/node@^16.18.39": - version "16.18.48" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.48.tgz#3bc872236cdb31cb51024d8875d655e25db489a4" - integrity sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q== + version "16.18.57" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.57.tgz#1ba31c0e5c403aab90a3b7826576e6782ded779b" + integrity sha512-piPoDozdPaX1hNWFJQzzgWqE40gh986VvVx/QO9RU4qYRE55ld7iepDVgZ3ccGUw0R4wge0Oy1dd+3xOQNkkUQ== "@types/plotly.js@*": - version "2.12.26" - resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-2.12.26.tgz#42c7b98a462645af56bdcb00349cf0beaae96fe1" - integrity sha512-vP1iaVL4HHYSbugv49pwtLL6D9CSqOnQLjiRRdRYjVMEDbjIWhMgxc49BJAxSUShupiJHDp35e0WJS9SwIB2WA== + version "2.12.27" + resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-2.12.27.tgz#4f0a9ef660504670a5f0bf44d1b1f2911d79b0b7" + integrity sha512-Ah7XuePFNxu2XAHG79GeKN/Ky8dZ0k6hzy49da6AeZFrTqO5wDbtJovp3co3C+iRitp8tA6rIxkltiJ3cjsQWw== "@types/prop-types@*": - version "15.7.5" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + version "15.7.8" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3" + integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ== "@types/react-plotly.js@^2.5.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@types/react-plotly.js/-/react-plotly.js-2.6.0.tgz#1b856c2ed1219babda3e95ef3270091f156ff987" - integrity sha512-nJJ57U0/CNDAO+F3dpnMgM8PtjLE/O1I3O6gq4+5Q13uKqrPnHGYOttfdzQJ4D7KYgF609miVzEYakUS2zds8w== + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/react-plotly.js/-/react-plotly.js-2.6.1.tgz#243019c11f30c19179095df9080a70d35ec33af8" + integrity sha512-vFJZRCC2Pav0NdrFm0grPMm9+67ejGZZglDBWqo+J6VFbB4CAatjoNiowfardznuujaaoDNoZ4MSCFwYyVk4aA== dependencies: "@types/plotly.js" "*" "@types/react" "*" "@types/react-test-renderer@^16.9.1": - version "16.9.6" - resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.6.tgz#cacda4066a869f562c99a2158fda4609a971a231" - integrity sha512-EZbtXp2xiuxIYJuyzXnG+5rIK34oGmmcW95FG/x3yN+p0j+jgC947MjpgFuGwYzcLZVymmvXlOADaEtUOiP6GA== + version "16.9.8" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.8.tgz#42268fe29cb6519c2bfe057b047be7063aa93564" + integrity sha512-5zmgK3rWLQLX8VwcK94KqCnXTmirDFgsJOElNbnvNJ7v6QIg3SBqZgH/b5Z5VVQVCTiXc4ZzETF4H0yq3VwHCQ== dependencies: "@types/react" "^16" "@types/react@*": - version "18.2.21" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" - integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA== + version "18.2.25" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.25.tgz#99fa44154132979e870ff409dc5b6e67f06f0199" + integrity sha512-24xqse6+VByVLIr+xWaQ9muX1B4bXJKXBbjszbld/UEDslGLY53+ZucF44HCmLbMPejTzGG9XgR+3m2/Wqu1kw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" csstype "^3.0.2" "@types/react@^16": - version "16.14.46" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.46.tgz#42ac91aece416176e6b6127cd9ec9e381ea67e16" - integrity sha512-Am4pyXMrr6cWWw/TN3oqHtEZl0j+G6Up/O8m65+xF/3ZaUgkv1GAtTPWw4yNRmH0HJXmur6xKCKoMo3rBGynuw== + version "16.14.48" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.48.tgz#bce2e699e9dbf8d9dc33ba9167225758c94e1d6a" + integrity sha512-7HP7K9IyuP6CpxEHmfRPEl21pwra+nSgZHXhyq7WOkxhIGYtSpIHJBijh4zuScgelrPxsUXVPDRkSKHhT+6nkg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -349,9 +349,9 @@ sanitize-filename "*" "@types/scheduler@*": - version "0.16.3" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== + version "0.16.4" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.4.tgz#fedc3e5b15c26dc18faae96bf1317487cb3658cf" + integrity sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ== "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" @@ -359,9 +359,9 @@ integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" - integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.4.tgz#cd6531924f60834fa4a1b8081f9eecf9bb1117f0" + integrity sha512-jA2llq2zNkg8HrALI7DtWzhALcVH0l7i89yhY3iBdOz6cBPeACoFq+fkQrjHA39t1hnSFOboZ7A/AY5MMZSlag== "@types/unist@^2": version "2.0.8" @@ -369,21 +369,21 @@ integrity sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw== "@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + version "21.0.1" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b" + integrity sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ== "@types/yargs@^17.0.8": - version "17.0.24" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" - integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + version "17.0.26" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.26.tgz#388e5002a8b284ad7b4599ba89920a6d74d8d79a" + integrity sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw== dependencies: "@types/yargs-parser" "*" "@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + version "2.10.1" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.1.tgz#4e8f299f0934d60f36c74f59cb5a8483fd786691" + integrity sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw== dependencies: "@types/node" "*" @@ -442,6 +442,11 @@ anser@^1.4.1: resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" @@ -528,6 +533,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -637,6 +647,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -675,6 +690,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -743,9 +763,9 @@ chokidar@3.5.3: fsevents "~2.3.2" ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== classnames@^2.2, classnames@^2.2.5, classnames@^2.2.6: version "2.3.2" @@ -801,6 +821,15 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -898,13 +927,13 @@ csstype@^3.0.2: integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== cypress-watch-and-reload@^1.10.6: - version "1.10.7" - resolved "https://registry.yarnpkg.com/cypress-watch-and-reload/-/cypress-watch-and-reload-1.10.7.tgz#8c323e9889eb524aac87ac7333cb2c6a1ef50f2e" - integrity sha512-RNBG+J99yryOj0aarpZltT1ijHtWxw2N3Q9F6/ZGRr2HHtavLTfwV9wN/1dxinByWFZFBTQGLtKGo78wJT2O+g== + version "1.10.9" + resolved "https://registry.yarnpkg.com/cypress-watch-and-reload/-/cypress-watch-and-reload-1.10.9.tgz#2e3482d8c9a00452764860446a0305d3771d27d1" + integrity sha512-6FEh5lrn1JS3IAQxEUSJ/gCXtnMxracjpEPjqV54F7fWltxZ9a1PCjx57ONP+j2LNodMWHCS8FxPZ5F9KidyGQ== dependencies: async-wait-until "1.2.6" chokidar "3.5.3" - ws "8.14.0" + ws "8.14.2" cypress@^12.8.1: version "12.17.4" @@ -963,9 +992,9 @@ dashdash@^1.12.0: assert-plus "^1.0.0" dayjs@^1.10.4: - version "1.11.9" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" - integrity sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA== + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== debug@4.3.4, debug@^3.1.0, debug@^4.0.1, debug@^4.1.1, debug@^4.3.4: version "3.2.7" @@ -974,6 +1003,11 @@ debug@4.3.4, debug@^3.1.0, debug@^4.0.1, debug@^4.1.1, debug@^4.3.4: dependencies: ms "^2.1.1" +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -991,11 +1025,21 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +define-data-property@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" + integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-properties@^1.1.3, define-properties@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -1004,6 +1048,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -1044,7 +1093,7 @@ domhandler@^5.0, domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^3.0.1: +domutils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== @@ -1096,16 +1145,26 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" -entities@^4.2.0, entities@^4.4.0: +entities@^4.2.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-carriage@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.1.tgz#842658e5422497b1232585e517dc813fc6a86170" integrity sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw== +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1349,6 +1408,14 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -1358,6 +1425,11 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" @@ -1417,7 +1489,12 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== @@ -1460,6 +1537,18 @@ glob-parent@^5.0.0, glob-parent@^6.0.1, glob-parent@~5.1.2: dependencies: is-glob "^4.0.3" +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.3, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1486,6 +1575,13 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -1531,11 +1627,9 @@ has-tostringtag@^1.0.0: has-symbols "^1.0.2" has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" + version "1.0.4" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== hast-util-parse-selector@^2.0.0: version "2.2.5" @@ -1553,29 +1647,34 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== html-to-react@^1.3.4: - version "1.6.0" - resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.6.0.tgz#568c38b85e81086ed1dedacd031f42dd5616b557" - integrity sha512-W7HvCu2fipgz3F7fpEtIt2Ty6XcqFGQXOorR4+HQAk72y9mTtUH3BmJ43BEvXQHO+bt//z1Hbfe6JzojpSC/9w== + version "1.7.0" + resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.7.0.tgz#1664a0233a930ab1b12c442ddef0f1b72e7459f4" + integrity sha512-b5HTNaTGyOj5GGIMiWVr1k57egAZ/vGy0GGefnCQ1VW5hu9+eku8AXHtf2/DeD95cj/FKBKYa1J7SWBOX41yUQ== dependencies: domhandler "^5.0" - htmlparser2 "^8.0" + htmlparser2 "^9.0" lodash.camelcase "^4.3.0" -htmlparser2@^8.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== +htmlparser2@^9.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.0.0.tgz#e431142b7eeb1d91672742dea48af8ac7140cddb" + integrity sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ== dependencies: domelementtype "^2.3.0" domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" + domutils "^3.1.0" + entities "^4.5.0" http-signature@~1.3.6: version "1.3.6" @@ -1780,6 +1879,11 @@ is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-regex@^1.0.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -1834,9 +1938,9 @@ jest-dom@^4.0.0: integrity sha512-gBxYZlZB1Jgvf2gP2pRfjjUWF8woGBHj/g5rAQgFPB/0K2atGuhVcPO+BItyjWeKg9zM+dokgcMOH01vrWVMFA== jest-util@^29.0.0: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63" - integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA== + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: "@jest/types" "^29.6.3" "@types/node" "*" @@ -1850,6 +1954,13 @@ jest-util@^29.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -1977,6 +2088,13 @@ load-script@^1.0.0: resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -2007,7 +2125,7 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^4.0.0: +log-symbols@4.1.0, log-symbols@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -2115,7 +2233,7 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: +minimatch@5.0.1, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -2134,12 +2252,39 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +mocha@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" + integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mock-fs@^4.12.0: version "4.14.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== -ms@^2.1.1: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -2149,6 +2294,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2244,6 +2394,20 @@ ospath@^1.2.2: resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -2282,6 +2446,11 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -2328,9 +2497,9 @@ pify@^2.2.0: integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== plotly.js-dist@^2.2.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-2.26.0.tgz#fae335a23a3b6bd830aad48b9587e5caa5264f0f" - integrity sha512-CgVJfqKtxNA6nCVN4E/x7xFDLXjL8/tK55pA25XJrgwypnQN7S+/sF3yTVRk0OCimRPJ/cHlcffz6nIPK8J8MQ== + version "2.26.2" + resolved "https://registry.yarnpkg.com/plotly.js-dist/-/plotly.js-dist-2.26.2.tgz#d70146b5f4dc6be6023181bf3485c779ebf3c4fd" + integrity sha512-+edkDnFarCZxm+hHTRZfEs5GVUf+eNU9dfOR4jEfRC70T9K3ibsdTRRaYktdakomzyuOz8q2kHKon9llOArBGw== popper.js@^1.14.4, popper.js@^1.16.1: version "1.16.1" @@ -2427,6 +2596,13 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + re-resizable@^6.5.0: version "6.9.11" resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.11.tgz#f356e27877f12d926d076ab9ad9ff0b95912b475" @@ -2579,13 +2755,13 @@ regenerator-runtime@^0.14.0: integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== regexp.prototype.flags@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" - integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== dependencies: call-bind "^1.0.2" define-properties "^1.2.0" - functions-have-names "^1.2.3" + set-function-name "^2.0.0" regexpp@^2.0.1: version "2.0.1" @@ -2630,6 +2806,11 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -2716,7 +2897,7 @@ rxjs@^7.5.1: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2740,6 +2921,22 @@ semver@^5.5.0, semver@^6.1.2, semver@^7.5.2, semver@^7.5.3: dependencies: lru-cache "^6.0.0" +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2897,11 +3094,18 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== -strip-json-comments@^3.0.1: +strip-json-comments@3.1.1, strip-json-comments@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +supports-color@8.1.1, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -2916,13 +3120,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -3212,9 +3409,9 @@ vfile@^2.0.0: vfile-message "^1.0.0" vis-data@^7.1.2: - version "7.1.6" - resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.6.tgz#81dcf4d024d23183cacb680ad605e644cdd6ee6c" - integrity sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg== + version "7.1.7" + resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.7.tgz#e70475e69e01ed0f4535352ed0eae64a6337f298" + integrity sha512-Jfrb6Ixyr3jdqFgpCBWnzb4w7PdD3UjOY6vea9yXixoKSLveUj+rAuxByoRKRvdjhsRtsYCEXG6MXjjx+uvGvQ== vis-network@^9.0.0: version "9.1.6" @@ -3247,6 +3444,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -3286,10 +3488,10 @@ write@1.0.3: dependencies: mkdirp "^0.5.1" -ws@8.14.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.0.tgz#6c5792c5316dc9266ba8e780433fc45e6680aecd" - integrity sha512-WR0RJE9Ehsio6U4TuM+LmunEsjQ5ncHlw4sn9ihD6RoJKZrVyH9FWV3dmnwu8B2aNib1OvG2X6adUCyFpQyWcg== +ws@8.14.2: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== x-is-string@^0.1.0: version "0.1.0" @@ -3301,6 +3503,11 @@ xtend@^4.0.0, xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -3311,11 +3518,44 @@ yaml@2.3.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.0.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" @@ -3323,3 +3563,8 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==