diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts b/src/plugins/discover/public/embeddable/constants.ts similarity index 93% rename from src/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts rename to src/plugins/discover/public/embeddable/constants.ts index fe264fc193fc..82f428ea4e4b 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/private.d.ts +++ b/src/plugins/discover/public/embeddable/constants.ts @@ -28,4 +28,4 @@ * under the License. */ -export type IPrivate = (provider: (...injectable: any[]) => T) => T; +export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/index.ts b/src/plugins/discover/public/embeddable/index.ts similarity index 79% rename from src/plugins/opensearch_dashboards_legacy/public/utils/index.ts rename to src/plugins/discover/public/embeddable/index.ts index 6313548a1be1..ecb5e98f43c1 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/index.ts +++ b/src/plugins/discover/public/embeddable/index.ts @@ -28,10 +28,6 @@ * under the License. */ -export * from './system_api'; -// @ts-ignore -export { OsdAccessibleClickProvider } from './osd_accessible_click'; -// @ts-ignore -export { PrivateProvider, IPrivate } from './private'; -// @ts-ignore -export { registerListenEventListener } from './register_listen_event_listener'; +export { SEARCH_EMBEDDABLE_TYPE } from './constants'; +export * from './types'; +export * from './search_embeddable_factory'; diff --git a/src/plugins/discover/public/embeddable/search_embeddable.tsx b/src/plugins/discover/public/embeddable/search_embeddable.tsx new file mode 100644 index 000000000000..76b6b9f449c4 --- /dev/null +++ b/src/plugins/discover/public/embeddable/search_embeddable.tsx @@ -0,0 +1,408 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isEqual } from 'lodash'; +import * as Rx from 'rxjs'; +import { Subscription } from 'rxjs'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { i18n } from '@osd/i18n'; +import { UiActionsStart, APPLY_FILTER_TRIGGER } from '../../../ui_actions/public'; +import { RequestAdapter, Adapters } from '../../../inspector/public'; +import { + opensearchFilters, + Filter, + TimeRange, + FilterManager, + getTime, + Query, + IFieldType, +} from '../../../data/public'; +import { Container, Embeddable } from '../../../embeddable/public'; +import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; +import { getDefaultSort } from '../application/view_components/utils/get_default_sort'; +import { getSortForSearchSource } from '../application/view_components/utils/get_sort_for_search_source'; +import { + getRequestInspectorStats, + getResponseInspectorStats, + getServices, + IndexPattern, + ISearchSource, +} from '../opensearch_dashboards_services'; +import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +import { SortOrder } from '../saved_searches/types'; +import { SavedSearch } from '../saved_searches'; +import { + SAMPLE_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, +} from '../../common'; +import { SearchEmbeddableComponent } from './search_embeddable_component'; +import { DiscoverServices } from '../build_services'; +import * as columnActions from '../application/utils/state_management/common'; +import { buildColumns } from '../application/utils/columns'; + +export interface SearchProps { + columns?: string[]; + description?: string; + sort?: SortOrder[]; + onSort?: (sort: SortOrder[]) => void; + sharedItemTitle?: string; + inspectorAdapters?: Adapters; + onSetColumns?: (columns: string[]) => void; + onRemoveColumn?: (column: string) => void; + onAddColumn?: (column: string) => void; + onMoveColumn?: (column: string, index: number) => void; + onFilter?: (field: IFieldType, value: string[], operator: string) => void; + rows?: any[]; + indexPattern?: IndexPattern; + totalHitCount?: number; + isLoading?: boolean; + displayTimeColumn?: boolean; + services: DiscoverServices; +} + +interface SearchEmbeddableConfig { + savedSearch: SavedSearch; + editUrl: string; + editPath: string; + indexPatterns?: IndexPattern[]; + editable: boolean; + filterManager: FilterManager; + services: DiscoverServices; +} + +export class SearchEmbeddable + extends Embeddable + implements ISearchEmbeddable { + private readonly savedSearch: SavedSearch; + private inspectorAdaptors: Adapters; + private searchProps?: SearchProps; + private panelTitle: string = ''; + private filtersSearchSource?: ISearchSource; + private autoRefreshFetchSubscription?: Subscription; + private subscription?: Subscription; + public readonly type = SEARCH_EMBEDDABLE_TYPE; + private services: DiscoverServices; + private filterManager: FilterManager; + private abortController?: AbortController; + + private prevTimeRange?: TimeRange; + private prevFilters?: Filter[]; + private prevQuery?: Query; + + private node?: HTMLElement; + + constructor( + { + savedSearch, + editUrl, + editPath, + indexPatterns, + editable, + filterManager, + services, + }: SearchEmbeddableConfig, + initialInput: SearchInput, + private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], + parent?: Container + ) { + super( + initialInput, + { + defaultTitle: savedSearch.title, + editUrl, + editPath, + editApp: 'discover', + indexPatterns, + editable, + }, + parent + ); + + this.services = services; + this.filterManager = filterManager; + this.savedSearch = savedSearch; + this.inspectorAdaptors = { + requests: new RequestAdapter(), + }; + this.initializeSearchProps(); + + this.autoRefreshFetchSubscription = getServices() + .timefilter.getAutoRefreshFetch$() + .subscribe(this.fetch); + + this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => { + this.panelTitle = this.output.title || ''; + + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps); + } + }); + } + + public getInspectorAdapters() { + return this.inspectorAdaptors; + } + + public getSavedSearch() { + return this.savedSearch; + } + + /** + * + * @param {Element} domNode + */ + public render(node: HTMLElement) { + if (!this.searchProps) { + throw new Error('Search scope not defined'); + } + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + } + + public destroy() { + super.destroy(); + if (this.searchProps) { + delete this.searchProps; + } + if (this.subscription) { + this.subscription.unsubscribe(); + } + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + if (this.autoRefreshFetchSubscription) { + this.autoRefreshFetchSubscription.unsubscribe(); + } + if (this.abortController) this.abortController.abort(); + } + + private initializeSearchProps() { + const { searchSource } = this.savedSearch; + const indexPattern = searchSource.getField('index'); + if (!indexPattern) { + return; + } + + const sort = getDefaultSort( + indexPattern, + this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc') + ); + this.savedSearch.sort = sort; + + const searchProps: SearchProps = { + columns: this.savedSearch.columns, + sort: [], + inspectorAdapters: this.inspectorAdaptors, + rows: [], + description: this.savedSearch.description, + services: this.services, + indexPattern, + isLoading: false, + displayTimeColumn: !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + }; + + const timeRangeSearchSource = searchSource.create(); + timeRangeSearchSource.setField('filter', () => { + if (!this.searchProps || !this.input.timeRange) return; + return getTime(indexPattern, this.input.timeRange); + }); + + this.filtersSearchSource = searchSource.create(); + this.filtersSearchSource.setParent(timeRangeSearchSource); + + searchSource.setParent(this.filtersSearchSource); + + searchProps.onSort = (newSort) => { + this.updateInput({ sort: newSort }); + }; + + searchProps.onAddColumn = (columnName: string) => { + if (!searchProps.columns) { + return; + } + const updatedColumns = buildColumns( + columnActions.addColumn(searchProps.columns, { column: columnName }) + ); + this.updateInput({ columns: updatedColumns }); + }; + + searchProps.onRemoveColumn = (columnName: string) => { + if (!searchProps.columns) { + return; + } + const updatedColumns = columnActions.removeColumn(searchProps.columns, columnName); + const updatedSort = + searchProps.sort && searchProps.sort.length + ? searchProps.sort.filter((s) => s[0] !== columnName) + : []; + this.updateInput({ sort: updatedSort, columns: updatedColumns }); + }; + + searchProps.onMoveColumn = (columnName, newIndex: number) => { + if (!searchProps.columns) { + return; + } + const oldIndex = searchProps.columns.indexOf(columnName); + const updatedColumns = columnActions.reorderColumn(searchProps.columns, oldIndex, newIndex); + this.updateInput({ columns: updatedColumns }); + }; + + searchProps.onSetColumns = (columnNames: string[]) => { + const columns = buildColumns(columnNames); + this.updateInput({ columns }); + }; + + searchProps.onFilter = async (field, value, operator) => { + let filters = opensearchFilters.generateFilters( + this.filterManager, + field, + value, + operator, + indexPattern.id! + ); + filters = filters.map((filter) => ({ + ...filter, + $state: { store: opensearchFilters.FilterStateStore.APP_STATE }, + })); + + await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters, + }); + }; + + this.pushContainerStateParamsToProps(searchProps); + } + + public reload() { + if (this.searchProps) { + this.pushContainerStateParamsToProps(this.searchProps); + } + } + + private fetch = async () => { + if (!this.searchProps) return; + + const { searchSource } = this.savedSearch; + + // Abort any in-progress requests + if (this.abortController) this.abortController.abort(); + this.abortController = new AbortController(); + + searchSource.setField('size', getServices().uiSettings.get(SAMPLE_SIZE_SETTING)); + searchSource.setField( + 'sort', + getSortForSearchSource( + this.searchProps.sort, + this.searchProps.indexPattern, + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ) + ); + + // Log request to inspector + this.inspectorAdaptors.requests.reset(); + const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', { + defaultMessage: 'Data', + }); + const description = i18n.translate('discover.embeddable.inspectorRequestDescription', { + defaultMessage: 'This request queries OpenSearch to fetch the data for the search.', + }); + const inspectorRequest = this.inspectorAdaptors.requests.start(title, { description }); + inspectorRequest.stats(getRequestInspectorStats(searchSource)); + searchSource.getSearchRequestBody().then((body: Record) => { + inspectorRequest.json(body); + }); + this.updateOutput({ loading: true, error: undefined }); + this.searchProps!.isLoading = true; + + try { + // Make the request + const resp = await searchSource.fetch({ + abortSignal: this.abortController.signal, + }); + this.updateOutput({ loading: false, error: undefined }); + + // Log response to inspector + inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); + + this.searchProps!.rows = resp.hits.hits; + this.searchProps!.totalHitCount = resp.hits.total; + this.searchProps!.isLoading = false; + } catch (error) { + this.updateOutput({ loading: false, error }); + this.searchProps!.isLoading = false; + } + }; + + private renderComponent(node: HTMLElement, searchProps: SearchProps) { + if (!this.searchProps) { + return; + } + const props = { + searchProps, + }; + ReactDOM.render(, node); + } + + private async pushContainerStateParamsToProps(searchProps: SearchProps) { + const isFetchRequired = + !opensearchFilters.onlyDisabledFiltersChanged(this.input.filters, this.prevFilters) || + !isEqual(this.prevQuery, this.input.query) || + !isEqual(this.prevTimeRange, this.input.timeRange) || + !isEqual(searchProps.sort, this.input.sort || this.savedSearch.sort); + + // If there is column or sort data on the panel, that means the original columns or sort settings have + // been overridden in a dashboard. + searchProps.columns = this.input.columns || this.savedSearch.columns; + searchProps.sort = this.input.sort || this.savedSearch.sort; + searchProps.sharedItemTitle = this.panelTitle; + + if (isFetchRequired) { + this.filtersSearchSource!.setField('filter', this.input.filters); + this.filtersSearchSource!.setField('query', this.input.query); + this.prevFilters = this.input.filters; + this.prevQuery = this.input.query; + this.prevTimeRange = this.input.timeRange; + this.searchProps = searchProps; + + await this.fetch(); + } else if (this.searchProps) { + this.searchProps = searchProps; + } + + if (this.node) { + this.renderComponent(this.node, this.searchProps!); + } + } +} diff --git a/src/plugins/discover/public/embeddable/search_embeddable_factory.tsx b/src/plugins/discover/public/embeddable/search_embeddable_factory.tsx new file mode 100644 index 000000000000..8d99b87fbeb2 --- /dev/null +++ b/src/plugins/discover/public/embeddable/search_embeddable_factory.tsx @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@osd/i18n'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { getServices } from '../opensearch_dashboards_services'; +import { + EmbeddableFactoryDefinition, + Container, + ErrorEmbeddable, +} from '../../../embeddable/public'; +import { TimeRange } from '../../../data/public'; +import { SearchEmbeddable } from './search_embeddable'; +import { SearchInput, SearchOutput } from './types'; +import { SEARCH_EMBEDDABLE_TYPE } from './constants'; + +interface StartServices { + executeTriggerActions: UiActionsStart['executeTriggerActions']; + isEditable: () => boolean; +} + +export class SearchEmbeddableFactory + implements EmbeddableFactoryDefinition { + public readonly type = SEARCH_EMBEDDABLE_TYPE; + public readonly savedObjectMetaData = { + name: i18n.translate('discover.savedSearch.savedObjectName', { + defaultMessage: 'Saved search', + }), + type: 'search', + getIconForSavedObject: () => 'search', + }; + + constructor(private getStartServices: () => Promise) {} + + public canCreateNew() { + return false; + } + + public isEditable = async () => { + return (await this.getStartServices()).isEditable(); + }; + + public getDisplayName() { + return i18n.translate('discover.embeddable.search.displayName', { + defaultMessage: 'search', + }); + } + + public createFromSavedObject = async ( + savedObjectId: string, + input: Partial & { id: string; timeRange: TimeRange }, + parent?: Container + ): Promise => { + const services = getServices(); + const filterManager = services.filterManager; + const url = await services.getSavedSearchUrlById(savedObjectId); + const editUrl = services.addBasePath(`/app/data-explorer/discover${url}`); + + try { + const savedObject = await services.getSavedSearchById(savedObjectId); + const indexPattern = savedObject.searchSource.getField('index'); + const { executeTriggerActions } = await this.getStartServices(); + const { SearchEmbeddable: SearchEmbeddableClass } = await import('./search_embeddable'); + return new SearchEmbeddableClass( + { + savedSearch: savedObject, + editUrl, + editPath: url, + filterManager, + editable: services.capabilities.discover.save as boolean, + indexPatterns: indexPattern ? [indexPattern] : [], + services, + }, + input, + executeTriggerActions, + parent + ); + } catch (e) { + console.error(e); // eslint-disable-line no-console + return new ErrorEmbeddable(e, input, parent); + } + }; + + public async create(input: SearchInput) { + return new ErrorEmbeddable('Saved searches can only be created from a saved object', input); + } +} diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js b/src/plugins/discover/public/embeddable/types.ts similarity index 56% rename from src/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js rename to src/plugins/discover/public/embeddable/types.ts index 19652d94cf4f..24a1aac92b49 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/register_listen_event_listener.js +++ b/src/plugins/discover/public/embeddable/types.ts @@ -28,20 +28,35 @@ * under the License. */ -export function registerListenEventListener($rootScope) { - /** - * Helper that registers an event listener, and removes that listener when - * the $scope is destroyed. - * - * @param {EventEmitter} emitter - the event emitter to listen to - * @param {string} eventName - the event name - * @param {Function} handler - the event handler - * @return {undefined} - */ - $rootScope.constructor.prototype.$listen = function (emitter, eventName, handler) { - emitter.on(eventName, handler); - this.$on('$destroy', function () { - emitter.off(eventName, handler); - }); - }; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from 'src/plugins/embeddable/public'; +import { Filter, IIndexPattern, TimeRange, Query } from '../../../../data/public'; +import { SortOrder } from '../saved_searches/types'; +import { SavedSearch } from '../saved_searches'; + +export interface SearchInput extends EmbeddableInput { + timeRange: TimeRange; + query?: Query; + filters?: Filter[]; + hidePanelTitles?: boolean; + columns?: string[]; + sort?: SortOrder[]; +} + +export interface SearchOutput extends EmbeddableOutput { + editUrl: string; + indexPatterns?: IIndexPattern[]; + editable: boolean; +} + +export interface ISearchEmbeddable extends IEmbeddable { + getSavedSearch(): SavedSearch; +} + +export interface SearchEmbeddable extends Embeddable { + type: string; } diff --git a/src/plugins/discover_legacy/public/application/components/discover_legacy.tsx b/src/plugins/discover_legacy/public/application/components/discover_legacy.tsx new file mode 100644 index 000000000000..d4a3d235a188 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/components/discover_legacy.tsx @@ -0,0 +1,368 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import classNames from 'classnames'; +import { EuiButtonEmpty, EuiButtonIcon, EuiCallOut, EuiLink } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { IUiSettingsClient, MountPoint } from 'opensearch-dashboards/public'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader } from './timechart_header'; +import { DiscoverSidebar } from './sidebar'; +import { getServices, IndexPattern } from '../../opensearch_dashboards_services'; +// @ts-ignore +import { DiscoverNoResults } from '../angular/directives/no_results'; +import { DiscoverUninitialized } from '../angular/directives/uninitialized'; +import { DiscoverHistogram } from '../angular/directives/histogram'; +import { LoadingSpinner } from './loading_spinner/loading_spinner'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; +import { SkipBottomButton } from './skip_bottom_button'; +import { + IndexPatternField, + search, + ISearchSource, + TimeRange, + Query, + IndexPatternAttributes, +} from '../../../../data/public'; +import { Chart } from '../angular/helpers/point_series'; +import { AppState } from '../angular/discover_state'; +import { SavedSearch } from '../../saved_searches'; + +import { SavedObject } from '../../../../../core/types'; +import { Vis } from '../../../../visualizations/public'; +import { TopNavMenuData } from '../../../../navigation/public'; + +export interface DiscoverLegacyProps { + addColumn: (column: string) => void; + fetch: () => void; + fetchCounter: number; + fieldCounts: Record; + histogramData: Chart; + hits: number; + indexPattern: IndexPattern; + minimumVisibleRows: number; + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + onChangeInterval: (interval: string) => void; + onMoveColumn: (columns: string, newIdx: number) => void; + onRemoveColumn: (column: string) => void; + onSetColumns: (columns: string[]) => void; + onSkipBottomButtonClick: () => void; + onSort: (sort: string[][]) => void; + opts: { + savedSearch: SavedSearch; + config: IUiSettingsClient; + indexPatternList: Array>; + timefield: string; + sampleSize: number; + fixedScroll: (el: HTMLElement) => void; + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + }; + resetQuery: () => void; + resultState: string; + rows: Array>; + searchSource: ISearchSource; + setIndexPattern: (id: string) => void; + showSaveQuery: boolean; + state: AppState; + timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + timeRange?: { from: string; to: string }; + topNavMenu: TopNavMenuData[]; + updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + updateSavedQueryId: (savedQueryId?: string) => void; + vis?: Vis; +} + +const KEY_SHOW_NOTICE = 'discover:deprecation-notice:show'; + +export function DiscoverLegacy({ + addColumn, + fetch, + fetchCounter, + fieldCounts, + histogramData, + hits, + indexPattern, + minimumVisibleRows, + onAddFilter, + onChangeInterval, + onMoveColumn, + onRemoveColumn, + onSkipBottomButtonClick, + onSort, + opts, + resetQuery, + resultState, + rows, + searchSource, + setIndexPattern, + showSaveQuery, + state, + timefilterUpdateHandler, + timeRange, + topNavMenu, + updateQuery, + updateSavedQueryId, + vis, +}: DiscoverLegacyProps) { + const [isSidebarClosed, setIsSidebarClosed] = useState(false); + const [isCallOutVisible, setIsCallOutVisible] = useState( + localStorage.getItem(KEY_SHOW_NOTICE) !== 'false' + ); + const { TopNavMenu } = getServices().navigation.ui; + const { savedSearch, indexPatternList } = opts; + const bucketAggConfig = vis?.data?.aggs?.aggs[1]; + const bucketInterval = + bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? bucketAggConfig.buckets?.getInterval() + : undefined; + const [fixedScrollEl, setFixedScrollEl] = useState(); + + const closeCallOut = () => { + localStorage.setItem(KEY_SHOW_NOTICE, 'false'); + setIsCallOutVisible(false); + }; + + let callOut; + + if (isCallOutVisible) { + callOut = ( +
+ +

+ To provide feedback,{' '} + + open an issue + + . +

+
+
+ ); + } + + useEffect(() => (fixedScrollEl ? opts.fixedScroll(fixedScrollEl) : undefined), [ + fixedScrollEl, + opts, + ]); + const fixedScrollRef = useCallback( + (node: HTMLElement) => { + if (node !== null) { + setFixedScrollEl(node); + } + }, + [setFixedScrollEl] + ); + const sidebarClassName = classNames({ + closed: isSidebarClosed, + }); + + const mainSectionClassName = classNames({ + 'col-md-10': !isSidebarClosed, + 'col-md-12': isSidebarClosed, + }); + + return ( + +
+

{savedSearch.title}

+
+ +
+
+
+
+ {!isSidebarClosed && ( +
+ +
+ )} + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + className="dscCollapsibleSidebar__collapseButton euiButtonIcon--auto" + /> +
+
+ {callOut} + {resultState === 'none' && ( + + )} + {resultState === 'uninitialized' && } + {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} + +
+ +
+
+ {resultState === 'ready' && ( +
+ + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} + /> + {opts.timefield && ( + + )} + + {opts.timefield && ( +
+ {vis && rows.length !== 0 && ( +
+ +
+ )} +
+ )} + +
+
+

+ +

+ {rows && rows.length && ( +
+ + + ​ + + {rows.length === opts.sampleSize && ( +
+ + + window.scrollTo(0, 0)}> + + +
+ )} +
+ )} +
+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/plugins/opensearch_dashboards_legacy/public/index.ts b/src/plugins/opensearch_dashboards_legacy/public/index.ts index 453bfd64dc82..cdbdb7eee6a9 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/index.ts +++ b/src/plugins/opensearch_dashboards_legacy/public/index.ts @@ -37,4 +37,3 @@ export const plugin = (initializerContext: PluginInitializerContext) => export * from './plugin'; export * from './notify'; -export * from './utils'; diff --git a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts b/src/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts index beb6f81e3ec7..54afa7c128ff 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts +++ b/src/plugins/opensearch_dashboards_legacy/public/notify/lib/add_fatal_error.ts @@ -29,21 +29,15 @@ */ import { FatalErrorsSetup } from '../../../../../core/public'; -import { - AngularHttpError, - formatAngularHttpError, - isAngularHttpError, -} from './format_angular_http_error'; +/** + * Adds an error to the list of fatal errors. + * @deprecated Use `core.fatalErrors.add` instead + */ export function addFatalError( - fatalErrors: FatalErrorsSetup, - error: AngularHttpError | Error | string, - location?: string -) { - // add support for angular http errors to newPlatformFatalErrors - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - - fatalErrors.add(error, location); -} + fatalErrors: FatalErrorsSetup, + error: Error | string, + location?: string + ) { + fatalErrors.add(error, location); + } \ No newline at end of file diff --git a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts b/src/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts deleted file mode 100644 index 68b3701814b1..000000000000 --- a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/format_angular_http_error.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@osd/i18n'; -import { IHttpResponse } from 'angular'; - -export type AngularHttpError = IHttpResponse<{ message: string }>; - -export function isAngularHttpError(error: any): error is AngularHttpError { - return ( - error && - typeof error.status === 'number' && - typeof error.statusText === 'string' && - error.data && - typeof error.data.message === 'string' - ); -} - -export function formatAngularHttpError(error: AngularHttpError) { - // is an Angular $http "error object" - if (error.status === -1) { - // status = -1 indicates that the request was failed to reach the server - return i18n.translate( - 'opensearch_dashboards_legacy.notify.fatalError.unavailableServerErrorMessage', - { - defaultMessage: - 'An HTTP request has failed to connect. ' + - 'Please check if the OpenSearch Dashboards server is running and that your browser has a working connection, ' + - 'or contact your system administrator.', - } - ); - } - - return i18n.translate('opensearch_dashboards_legacy.notify.fatalError.errorStatusMessage', { - defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', - values: { - errStatus: error.status, - errStatusText: error.statusText, - errMessage: error.data.message, - }, - }); -} diff --git a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts b/src/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts index 22a8631dfee3..685bd6c3b62a 100644 --- a/src/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts +++ b/src/plugins/opensearch_dashboards_legacy/public/notify/lib/index.ts @@ -31,9 +31,3 @@ export { formatOpenSearchMsg } from './format_opensearch_msg'; export { formatMsg } from './format_msg'; export { formatStack } from './format_stack'; -export { - isAngularHttpError, - formatAngularHttpError, - AngularHttpError, -} from './format_angular_http_error'; -export { addFatalError } from './add_fatal_error'; diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js b/src/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js deleted file mode 100644 index 6c49ff8de4bb..000000000000 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/osd_accessible_click.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { accessibleClickKeys, keys } from '@elastic/eui'; - -export function OsdAccessibleClickProvider() { - return { - restrict: 'A', - controller: ($element) => { - $element.on('keydown', (e) => { - // Prevent a scroll from occurring if the user has hit space. - if (e.key === keys.SPACE) { - e.preventDefault(); - } - }); - }, - link: (scope, element, attrs) => { - // The whole point of this directive is to hack in functionality that native buttons provide - // by default. - const elementType = element.prop('tagName'); - - if (elementType === 'BUTTON') { - throw new Error(`osdAccessibleClick doesn't need to be used on a button.`); - } - - if (elementType === 'A' && attrs.href !== undefined) { - throw new Error( - `osdAccessibleClick doesn't need to be used on a link if it has a href attribute.` - ); - } - - // We're emulating a click action, so we should already have a regular click handler defined. - if (!attrs.ngClick) { - throw new Error('osdAccessibleClick requires ng-click to be defined on its element.'); - } - - // If the developer hasn't already specified attributes required for accessibility, add them. - if (attrs.tabindex === undefined) { - element.attr('tabindex', '0'); - } - - if (attrs.role === undefined) { - element.attr('role', 'button'); - } - - element.on('keyup', (e) => { - // Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress. - if (accessibleClickKeys[e.key]) { - // Delegate to the click handler on the element (assumed to be ng-click). - element.click(); - } - }); - }, - }; -} diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/private.js b/src/plugins/opensearch_dashboards_legacy/public/utils/private.js deleted file mode 100644 index 1a3a0a596559..000000000000 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/private.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * # `Private()` - * Private module loader, used to merge angular and require js dependency styles - * by allowing a require.js module to export a single provider function that will - * create a value used within an angular application. This provider can declare - * angular dependencies by listing them as arguments, and can be require additional - * Private modules. - * - * ## Define a private module provider: - * ```js - * export default function PingProvider($http) { - * this.ping = function () { - * return $http.head('/health-check'); - * }; - * }; - * ``` - * - * ## Require a private module: - * ```js - * export default function ServerHealthProvider(Private, Promise) { - * let ping = Private(require('ui/ping')); - * return { - * check: Promise.method(function () { - * let attempts = 0; - * return (function attempt() { - * attempts += 1; - * return ping.ping() - * .catch(function (err) { - * if (attempts < 3) return attempt(); - * }) - * }()) - * .then(function () { - * return true; - * }) - * .catch(function () { - * return false; - * }); - * }) - * } - * }; - * ``` - * - * # `Private.stub(provider, newInstance)` - * `Private.stub()` replaces the instance of a module with another value. This is all we have needed until now. - * - * ```js - * beforeEach(inject(function ($injector, Private) { - * Private.stub( - * // since this module just exports a function, we need to change - * // what Private returns in order to modify it's behavior - * require('ui/agg_response/hierarchical/_build_split'), - * sinon.stub().returns(fakeSplit) - * ); - * })); - * ``` - * - * # `Private.swap(oldProvider, newProvider)` - * This new method does an 1-for-1 swap of module providers, unlike `stub()` which replaces a modules instance. - * Pass the module you want to swap out, and the one it should be replaced with, then profit. - * - * Note: even though this example shows `swap()` being called in a config - * function, it can be called from anywhere. It is particularly useful - * in this scenario though. - * - * ```js - * beforeEach(module('opensearchDashboards', function (PrivateProvider) { - * PrivateProvider.swap( - * function StubbedRedirectProvider($decorate) { - * // $decorate is a function that will instantiate the original module when called - * return sinon.spy($decorate()); - * } - * ); - * })); - * ``` - * - * @param {[type]} prov [description] - */ -import _ from 'lodash'; - -const nextId = _.partial(_.uniqueId, 'privateProvider#'); - -function name(fn) { - return fn.name || fn.toString().split('\n').shift(); -} - -export function PrivateProvider() { - const provider = this; - - // one cache/swaps per Provider - const cache = {}; - const swaps = {}; - - // return the uniq id for this function - function identify(fn) { - if (typeof fn !== 'function') { - throw new TypeError('Expected private module "' + fn + '" to be a function'); - } - - if (fn.$$id) return fn.$$id; - else return (fn.$$id = nextId()); - } - - provider.stub = function (fn, instance) { - cache[identify(fn)] = instance; - return instance; - }; - - provider.swap = function (fn, prov) { - const id = identify(fn); - swaps[id] = prov; - }; - - provider.$get = [ - '$injector', - function PrivateFactory($injector) { - // prevent circular deps by tracking where we came from - const privPath = []; - const pathToString = function () { - return privPath.map(name).join(' -> '); - }; - - // call a private provider and return the instance it creates - function instantiate(prov, locals) { - if (~privPath.indexOf(prov)) { - throw new Error( - 'Circular reference to "' + - name(prov) + - '"' + - ' found while resolving private deps: ' + - pathToString() - ); - } - - privPath.push(prov); - - const context = {}; - let instance = $injector.invoke(prov, context, locals); - if (!_.isObject(instance)) instance = context; - - privPath.pop(); - return instance; - } - - // retrieve an instance from cache or create and store on - function get(id, prov, $delegateId, $delegateProv) { - if (cache[id]) return cache[id]; - - let instance; - - if ($delegateId != null && $delegateProv != null) { - instance = instantiate(prov, { - $decorate: _.partial(get, $delegateId, $delegateProv), - }); - } else { - instance = instantiate(prov); - } - - return (cache[id] = instance); - } - - // main api, get the appropriate instance for a provider - function Private(prov) { - let id = identify(prov); - let $delegateId; - let $delegateProv; - - if (swaps[id]) { - $delegateId = id; - $delegateProv = prov; - - prov = swaps[$delegateId]; - id = identify(prov); - } - - return get(id, prov, $delegateId, $delegateProv); - } - - Private.stub = provider.stub; - Private.swap = provider.swap; - - return Private; - }, - ]; -} diff --git a/src/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts b/src/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts deleted file mode 100644 index 2675bbc084fb..000000000000 --- a/src/plugins/opensearch_dashboards_legacy/public/utils/system_api.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IRequestConfig } from 'angular'; - -const SYSTEM_REQUEST_HEADER_NAME = 'osd-system-request'; -const LEGACY_SYSTEM_API_HEADER_NAME = 'osd-system-api'; - -/** - * Adds a custom header designating request as system API - * @param originalHeaders Object representing set of headers - * @return Object representing set of headers, with system API header added in - */ -export function addSystemApiHeader(originalHeaders: Record) { - const systemApiHeaders = { - [SYSTEM_REQUEST_HEADER_NAME]: true, - }; - return { - ...originalHeaders, - ...systemApiHeaders, - }; -} - -/** - * Returns true if request is a system API request; false otherwise - * - * @param request Object Request object created by $http service - * @return true if request is a system API request; false otherwise - */ -export function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return ( - headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) - ); -}