{
+ let registry: any[] = [];
+
+ return {
+ getServices: () => ({
+ metadata: {
+ branch: 'test',
+ },
+ data: {
+ search: {
+ search: mockSearchApi,
+ },
+ },
+ }),
+ getDocViewsRegistry: () => ({
+ addDocView(view: any) {
+ registry.push(view);
+ },
+ getDocViewsSorted() {
+ return registry;
+ },
+ resetRegistry: () => {
+ registry = [];
+ },
+ }),
+ getDocViewsLinksRegistry: () => ({
+ addDocViewLink(view: any) {
+ registry.push(view);
+ },
+ getDocViewsLinksSorted() {
+ return registry;
+ },
+ resetRegistry: () => {
+ registry = [];
+ },
+ }),
+ };
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const waitForPromises = async () =>
+ act(async () => {
+ await new Promise((resolve) => setTimeout(resolve));
+ });
+
+/**
+ * this works but logs ugly error messages until we're using React 16.9
+ * should be adapted when we upgrade
+ */
+async function mountDoc(update = false, indexPatternGetter: any = null) {
+ const indexPattern = {
+ getComputedFields: () => [],
+ };
+ const indexPatternService = {
+ get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)),
+ } as any;
+
+ const props = {
+ id: '1',
+ index: 'index1',
+ indexPatternId: 'xyz',
+ indexPatternService,
+ } as DocProps;
+ let comp!: ReactWrapper;
+ await act(async () => {
+ comp = mountWithIntl(
);
+ if (update) comp.update();
+ });
+ if (update) {
+ await waitForPromises();
+ comp.update();
+ }
+ return comp;
+}
+
+describe('Test of
of Discover', () => {
+ test('renders loading msg', async () => {
+ const comp = await mountDoc();
+ expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1);
+ });
+
+ test('renders IndexPattern notFound msg', async () => {
+ const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' }));
+ const comp = await mountDoc(true, indexPatternGetter);
+ expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1);
+ });
+
+ test('renders notFound msg', async () => {
+ mockSearchApi.mockImplementation(() => throwError({ status: 404 }));
+ const comp = await mountDoc(true);
+ expect(findTestSubject(comp, 'doc-msg-notFound').length).toBe(1);
+ });
+
+ test('renders error msg', async () => {
+ mockSearchApi.mockImplementation(() => throwError({ error: 'something else' }));
+ const comp = await mountDoc(true);
+ expect(findTestSubject(comp, 'doc-msg-error').length).toBe(1);
+ });
+
+ test('renders opensearch hit ', async () => {
+ mockSearchApi.mockImplementation(() =>
+ of({ rawResponse: { hits: { total: 1, hits: [{ _id: 1, _source: { test: 1 } }] } } })
+ );
+ const comp = await mountDoc(true);
+ expect(findTestSubject(comp, 'doc-hit').length).toBe(1);
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/doc/doc.tsx b/src/plugins/discover_legacy/public/application/components/doc/doc.tsx
new file mode 100644
index 000000000000..204a16d64757
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc/doc.tsx
@@ -0,0 +1,140 @@
+/*
+ * 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 from 'react';
+import { FormattedMessage, I18nProvider } from '@osd/i18n/react';
+import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic/eui';
+import { IndexPatternsContract } from 'src/plugins/data/public';
+import { OpenSearchRequestState, useOpenSearchDocSearch } from './use_opensearch_doc_search';
+import { DocViewer } from '../doc_viewer/doc_viewer';
+
+export interface DocProps {
+ /**
+ * Id of the doc in OpenSearch
+ */
+ id: string;
+ /**
+ * Index in OpenSearch to query
+ */
+ index: string;
+ /**
+ * IndexPattern ID used to get IndexPattern entity
+ * that's used for adding additional fields (stored_fields, script_fields, docvalue_fields)
+ */
+ indexPatternId: string;
+ /**
+ * IndexPatternService to get a given index pattern by ID
+ */
+ indexPatternService: IndexPatternsContract;
+}
+
+export function Doc(props: DocProps) {
+ const [reqState, hit, indexPattern] = useOpenSearchDocSearch(props);
+ return (
+
+
+ {reqState === OpenSearchRequestState.NotFoundIndexPattern && (
+
+ }
+ />
+ )}
+ {reqState === OpenSearchRequestState.NotFound && (
+
+ }
+ >
+
+
+ )}
+
+ {reqState === OpenSearchRequestState.Error && (
+
+ }
+ >
+ {' '}
+
+
+
+
+ )}
+
+ {reqState === OpenSearchRequestState.Loading && (
+
+ {' '}
+
+
+ )}
+
+ {reqState === OpenSearchRequestState.Found && hit !== null && indexPattern && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx
new file mode 100644
index 000000000000..cb716a4f17cb
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.test.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import {
+ buildSearchBody,
+ useOpenSearchDocSearch,
+ OpenSearchRequestState,
+} from './use_opensearch_doc_search';
+import { DocProps } from './doc';
+import { Observable } from 'rxjs';
+
+const mockSearchResult = new Observable();
+
+jest.mock('../../../opensearch_dashboards_services', () => ({
+ getServices: () => ({
+ data: {
+ search: {
+ search: jest.fn(() => {
+ return mockSearchResult;
+ }),
+ },
+ },
+ }),
+}));
+
+describe('Test of
helper / hook', () => {
+ test('buildSearchBody', () => {
+ const indexPattern = {
+ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }),
+ } as any;
+ const actual = buildSearchBody('1', indexPattern);
+ expect(actual).toMatchInlineSnapshot(`
+ Object {
+ "_source": true,
+ "docvalue_fields": Array [],
+ "query": Object {
+ "ids": Object {
+ "values": Array [
+ "1",
+ ],
+ },
+ },
+ "script_fields": Array [],
+ "stored_fields": Array [],
+ }
+ `);
+ });
+
+ test('useOpenSearchDocSearch', async () => {
+ const indexPattern = {
+ getComputedFields: () => [],
+ };
+ const indexPatternService = {
+ get: jest.fn(() => Promise.resolve(indexPattern)),
+ } as any;
+ const props = {
+ id: '1',
+ index: 'index1',
+ indexPatternId: 'xyz',
+ indexPatternService,
+ } as DocProps;
+ let hook;
+ await act(async () => {
+ hook = renderHook((p: DocProps) => useOpenSearchDocSearch(p), { initialProps: props });
+ });
+ // @ts-ignore
+ expect(hook.result.current).toEqual([OpenSearchRequestState.Loading, null, indexPattern]);
+ expect(indexPatternService.get).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts
new file mode 100644
index 000000000000..b5ca9fec1c2f
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc/use_opensearch_doc_search.ts
@@ -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 { useEffect, useState } from 'react';
+import { IndexPattern, getServices } from '../../../opensearch_dashboards_services';
+import { DocProps } from './doc';
+import { OpenSearchSearchHit } from '../../doc_views/doc_views_types';
+
+export enum OpenSearchRequestState {
+ Loading,
+ NotFound,
+ Found,
+ Error,
+ NotFoundIndexPattern,
+}
+
+/**
+ * helper function to build a query body for OpenSearch
+ * https://opensearch.org/docs/latest/opensearch/query-dsl/index/
+ */
+export function buildSearchBody(id: string, indexPattern: IndexPattern): Record
{
+ const computedFields = indexPattern.getComputedFields();
+
+ return {
+ query: {
+ ids: {
+ values: [id],
+ },
+ },
+ stored_fields: computedFields.storedFields,
+ _source: true,
+ script_fields: computedFields.scriptFields,
+ docvalue_fields: computedFields.docvalueFields,
+ };
+}
+
+/**
+ * Custom react hook for querying a single doc in OpenSearch
+ */
+export function useOpenSearchDocSearch({
+ id,
+ index,
+ indexPatternId,
+ indexPatternService,
+}: DocProps): [OpenSearchRequestState, OpenSearchSearchHit | null, IndexPattern | null] {
+ const [indexPattern, setIndexPattern] = useState(null);
+ const [status, setStatus] = useState(OpenSearchRequestState.Loading);
+ const [hit, setHit] = useState(null);
+
+ useEffect(() => {
+ async function requestData() {
+ try {
+ const indexPatternEntity = await indexPatternService.get(indexPatternId);
+ setIndexPattern(indexPatternEntity);
+
+ const { rawResponse } = await getServices()
+ .data.search.search({
+ dataSourceId: indexPatternEntity.dataSourceRef?.id,
+ params: {
+ index,
+ body: buildSearchBody(id, indexPatternEntity),
+ },
+ })
+ .toPromise();
+
+ const hits = rawResponse.hits;
+
+ if (hits?.hits?.[0]) {
+ setStatus(OpenSearchRequestState.Found);
+ setHit(hits.hits[0]);
+ } else {
+ setStatus(OpenSearchRequestState.NotFound);
+ }
+ } catch (err) {
+ if (err.savedObjectId) {
+ setStatus(OpenSearchRequestState.NotFoundIndexPattern);
+ } else if (err.status === 404) {
+ setStatus(OpenSearchRequestState.NotFound);
+ } else {
+ setStatus(OpenSearchRequestState.Error);
+ }
+ }
+ }
+ requestData();
+ }, [id, index, indexPatternId, indexPatternService]);
+ return [status, hit, indexPattern];
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap
new file mode 100644
index 000000000000..cc1647fe264e
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap
@@ -0,0 +1,56 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render with 3 different tabs 1`] = `
+
+ ,
+ "id": "osd_doc_viewer_tab_0",
+ "name": "Render function",
+ },
+ Object {
+ "content": ,
+ "id": "osd_doc_viewer_tab_1",
+ "name": "React component",
+ },
+ Object {
+ "content": ,
+ "id": "osd_doc_viewer_tab_2",
+ "name": "Invalid doc view",
+ },
+ ]
+ }
+ />
+
+`;
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap
new file mode 100644
index 000000000000..31509659ce41
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Mounting and unmounting DocViewerRenderTab 1`] = `
+[MockFunction] {
+ "calls": Array [
+ Array [
+ ,
+ Object {
+ "hit": Object {},
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": [MockFunction],
+ },
+ ],
+}
+`;
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss
new file mode 100644
index 000000000000..91b66fc84297
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.scss
@@ -0,0 +1,72 @@
+.osdDocViewerTable {
+ margin-top: $euiSizeS;
+}
+
+.osdDocViewer {
+ pre,
+ .osdDocViewer__value {
+ display: inline-block;
+ word-break: break-all;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ color: $euiColorFullShade;
+ vertical-align: top;
+ padding-top: 2px;
+ }
+
+ .osdDocViewer__field {
+ padding-top: 8px;
+ }
+
+ .dscFieldName {
+ color: $euiColorDarkShade;
+ }
+
+ td,
+ pre {
+ font-family: $euiCodeFontFamily;
+ }
+
+ tr:first-child td {
+ border-top-color: transparent;
+ }
+
+ tr:hover {
+ .osdDocViewer__actionButton {
+ opacity: 1;
+ }
+ }
+}
+
+.osdDocViewer__buttons,
+.osdDocViewer__field {
+ white-space: nowrap;
+}
+
+.osdDocViewer__buttons {
+ width: 60px;
+
+ // Show all icons if one is focused,
+ // IE doesn't support, but the fallback is just the focused button becomes visible
+ &:focus-within {
+ .osdDocViewer__actionButton {
+ opacity: 1;
+ }
+ }
+}
+
+.osdDocViewer__field {
+ width: 160px;
+}
+
+.osdDocViewer__actionButton {
+ opacity: 0;
+
+ &:focus {
+ opacity: 1;
+ }
+}
+
+.osdDocViewer__warning {
+ margin-right: $euiSizeS;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx
new file mode 100644
index 000000000000..ccab0be41ed2
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * 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 from 'react';
+import { mount, shallow } from 'enzyme';
+import { DocViewer } from './doc_viewer';
+import { findTestSubject } from 'test_utils/helpers';
+import { getDocViewsRegistry } from '../../../opensearch_dashboards_services';
+import { DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+jest.mock('../../../opensearch_dashboards_services', () => {
+ let registry: any[] = [];
+ return {
+ getDocViewsRegistry: () => ({
+ addDocView(view: any) {
+ registry.push(view);
+ },
+ getDocViewsSorted() {
+ return registry;
+ },
+ resetRegistry: () => {
+ registry = [];
+ },
+ }),
+ };
+});
+
+beforeEach(() => {
+ (getDocViewsRegistry() as any).resetRegistry();
+ jest.clearAllMocks();
+});
+
+test('Render with 3 different tabs', () => {
+ const registry = getDocViewsRegistry();
+ registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() });
+ registry.addDocView({ order: 20, title: 'React component', component: () => test
});
+ registry.addDocView({ order: 30, title: 'Invalid doc view' });
+
+ const renderProps = { hit: {} } as DocViewRenderProps;
+
+ const wrapper = shallow();
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('Render with 1 tab displaying error message', () => {
+ function SomeComponent() {
+ // this is just a placeholder
+ return null;
+ }
+
+ const registry = getDocViewsRegistry();
+ registry.addDocView({
+ order: 10,
+ title: 'React component',
+ component: SomeComponent,
+ });
+
+ const renderProps = { hit: {} } as DocViewRenderProps;
+ const errorMsg = 'Catch me if you can!';
+
+ const wrapper = mount();
+ const error = new Error(errorMsg);
+ wrapper.find(SomeComponent).simulateError(error);
+ const errorMsgComponent = findTestSubject(wrapper, 'docViewerError');
+ expect(errorMsgComponent.text()).toMatch(new RegExp(`${errorMsg}`));
+});
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx
new file mode 100644
index 000000000000..d165c9bd05b8
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 './doc_viewer.scss';
+import React from 'react';
+import { EuiTabbedContent } from '@elastic/eui';
+import { getDocViewsRegistry } from '../../../opensearch_dashboards_services';
+import { DocViewerTab } from './doc_viewer_tab';
+import { DocView, DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+/**
+ * Rendering tabs with different views of 1 OpenSearch hit in Discover.
+ * The tabs are provided by the `docs_views` registry.
+ * A view can contain a React `component`, or any JS framework by using
+ * a `render` function.
+ */
+export function DocViewer(renderProps: DocViewRenderProps) {
+ const docViewsRegistry = getDocViewsRegistry();
+ const tabs = docViewsRegistry
+ .getDocViewsSorted(renderProps.hit)
+ .map(({ title, render, component }: DocView, idx: number) => {
+ return {
+ id: `osd_doc_viewer_tab_${idx}`,
+ name: title,
+ content: (
+
+ ),
+ };
+ });
+
+ if (!tabs.length) {
+ // There there's a minimum of 2 tabs active in Discover.
+ // This condition takes care of unit tests with 0 tabs.
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx
new file mode 100644
index 000000000000..1cb14d191a57
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_error.tsx
@@ -0,0 +1,48 @@
+/*
+ * 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 from 'react';
+import { EuiCallOut, EuiCodeBlock } from '@elastic/eui';
+import { formatMsg, formatStack } from '../../../../../opensearch_dashboards_legacy/public';
+
+interface Props {
+ error: Error | string;
+}
+
+export function DocViewerError({ error }: Props) {
+ const errMsg = formatMsg(error);
+ const errStack = typeof error === 'object' ? formatStack(error) : '';
+
+ return (
+
+ {errStack && {errStack}}
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx
new file mode 100644
index 000000000000..83d857b24fc5
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.test.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 from 'react';
+import { mount } from 'enzyme';
+import { DocViewRenderTab } from './doc_viewer_render_tab';
+import { DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+test('Mounting and unmounting DocViewerRenderTab', () => {
+ const unmountFn = jest.fn();
+ const renderFn = jest.fn(() => unmountFn);
+ const renderProps = {
+ hit: {},
+ };
+
+ const wrapper = mount(
+
+ );
+
+ expect(renderFn).toMatchSnapshot();
+
+ wrapper.unmount();
+
+ expect(unmountFn).toBeCalled();
+});
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx
new file mode 100644
index 000000000000..edc7f40c5e43
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_render_tab.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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, { useRef, useEffect } from 'react';
+import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+interface Props {
+ render: DocViewRenderFn;
+ renderProps: DocViewRenderProps;
+}
+/**
+ * Responsible for rendering a tab provided by a render function.
+ * So any other framework can be used (E.g. legacy Angular 3rd party plugin code)
+ * The provided `render` function is called with a reference to the
+ * component's `HTMLDivElement` as 1st arg and `renderProps` as 2nd arg
+ */
+export function DocViewRenderTab({ render, renderProps }: Props) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (ref && ref.current) {
+ return render(ref.current, renderProps);
+ }
+ }, [render, renderProps]);
+ return ;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx
new file mode 100644
index 000000000000..6e7a5f1ac434
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer/doc_viewer_tab.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 from 'react';
+import { I18nProvider } from '@osd/i18n/react';
+import { DocViewRenderTab } from './doc_viewer_render_tab';
+import { DocViewerError } from './doc_viewer_render_error';
+import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+interface Props {
+ component?: React.ComponentType;
+ id: number;
+ render?: DocViewRenderFn;
+ renderProps: DocViewRenderProps;
+ title: string;
+}
+
+interface State {
+ error: Error | string;
+ hasError: boolean;
+}
+/**
+ * Renders the tab content of a doc view.
+ * Displays an error message when it encounters exceptions, thanks to
+ * Error Boundaries.
+ */
+export class DocViewerTab extends React.Component {
+ state = {
+ hasError: false,
+ error: '',
+ };
+
+ static getDerivedStateFromError(error: unknown) {
+ // Update state so the next render will show the fallback UI.
+ return { hasError: true, error };
+ }
+
+ shouldComponentUpdate(nextProps: Props, nextState: State) {
+ return (
+ nextProps.renderProps.hit._id !== this.props.renderProps.hit._id ||
+ nextProps.id !== this.props.id ||
+ nextState.hasError
+ );
+ }
+
+ render() {
+ const { component, render, renderProps, title } = this.props;
+ const { hasError, error } = this.state;
+
+ if (hasError && error) {
+ return ;
+ } else if (!render && !component) {
+ return (
+
+ );
+ }
+
+ if (render) {
+ // doc view is provided by a render function, e.g. for legacy Angular code
+ return ;
+ }
+
+ // doc view is provided by a react component
+
+ const Component = component as any;
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap
new file mode 100644
index 000000000000..95fb0c377180
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dont Render if generateCb.hide 1`] = `
+
+`;
+
+exports[`Render with 2 different links 1`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx
new file mode 100644
index 000000000000..8aba555b3a37
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { DocViewerLinks } from './doc_viewer_links';
+import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services';
+import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types';
+
+jest.mock('../../../opensearch_dashboards_services', () => {
+ let registry: any[] = [];
+ return {
+ getDocViewsLinksRegistry: () => ({
+ addDocViewLink(view: any) {
+ registry.push(view);
+ },
+ getDocViewsLinksSorted() {
+ return registry;
+ },
+ resetRegistry: () => {
+ registry = [];
+ },
+ }),
+ };
+});
+
+beforeEach(() => {
+ (getDocViewsLinksRegistry() as any).resetRegistry();
+ jest.clearAllMocks();
+});
+
+test('Render with 2 different links', () => {
+ const registry = getDocViewsLinksRegistry();
+ registry.addDocViewLink({
+ order: 10,
+ label: 'generateCb link',
+ generateCb: () => ({
+ url: 'aaa',
+ }),
+ });
+ registry.addDocViewLink({ order: 20, label: 'href link', href: 'bbb' });
+
+ const renderProps = { hit: {} } as DocViewLinkRenderProps;
+
+ const wrapper = shallow();
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('Dont Render if generateCb.hide', () => {
+ const registry = getDocViewsLinksRegistry();
+ registry.addDocViewLink({
+ order: 10,
+ label: 'generateCb link',
+ generateCb: () => ({
+ url: 'aaa',
+ hide: true,
+ }),
+ });
+
+ const renderProps = { hit: {} } as DocViewLinkRenderProps;
+
+ const wrapper = shallow();
+
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx
new file mode 100644
index 000000000000..9efb0693fde6
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/doc_viewer_links/doc_viewer_links.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiListGroupItem, EuiListGroupItemProps } from '@elastic/eui';
+import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services';
+import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types';
+
+export function DocViewerLinks(renderProps: DocViewLinkRenderProps) {
+ const listItems = getDocViewsLinksRegistry()
+ .getDocViewsLinksSorted()
+ .filter((item) => !(item.generateCb && item.generateCb(renderProps)?.hide))
+ .map((item) => {
+ const { generateCb, href, ...props } = item;
+ const listItem: EuiListGroupItemProps = {
+ 'data-test-subj': 'docTableRowAction',
+ ...props,
+ href: generateCb ? generateCb(renderProps).url : href,
+ };
+
+ return listItem;
+ });
+
+ return (
+
+ {listItems.map((item, index) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap
new file mode 100644
index 000000000000..cfd81a66acae
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FieldName renders a geo field, useShortDots is set to true 1`] = `
+
+
+
+
+
+
+
+
+
+ t.t.test
+
+
+
+
+`;
+
+exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = `
+
+
+
+
+
+
+
+
+
+ test.test.test
+
+
+
+
+`;
+
+exports[`FieldName renders a string field by providing fieldType and fieldName 1`] = `
+
+
+
+
+
+
+
+
+
+ test
+
+
+
+
+`;
diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx
new file mode 100644
index 000000000000..54dc902837d0
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/field_name/field_name.test.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 from 'react';
+import { render } from 'enzyme';
+import { FieldName } from './field_name';
+
+// Note that it currently provides just 2 basic tests, there should be more, but
+// the components involved will soon change
+test('FieldName renders a string field by providing fieldType and fieldName', () => {
+ const component = render();
+ expect(component).toMatchSnapshot();
+});
+
+test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => {
+ const component = render();
+ expect(component).toMatchSnapshot();
+});
+
+test('FieldName renders a geo field, useShortDots is set to true', () => {
+ const component = render(
+
+ );
+ expect(component).toMatchSnapshot();
+});
diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx b/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx
new file mode 100644
index 000000000000..bbd9ab79d0fb
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/field_name/field_name.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+
+import { FieldIcon, FieldIconProps } from '../../../../../opensearch_dashboards_react/public';
+import { shortenDottedString } from '../../helpers';
+import { getFieldTypeName } from './field_type_name';
+
+// properties fieldType and fieldName are provided in osd_doc_view
+// this should be changed when both components are deangularized
+interface Props {
+ fieldName: string;
+ fieldType: string;
+ useShortDots?: boolean;
+ fieldIconProps?: Omit;
+ scripted?: boolean;
+}
+
+export function FieldName({
+ fieldName,
+ fieldType,
+ useShortDots,
+ fieldIconProps,
+ scripted = false,
+}: Props) {
+ const typeName = getFieldTypeName(fieldType);
+ const displayName = useShortDots ? shortenDottedString(fieldName) : fieldName;
+
+ return (
+
+
+
+
+
+
+ {displayName}
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts b/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts
new file mode 100644
index 000000000000..38b18792d3e4
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/field_name/field_type_name.ts
@@ -0,0 +1,85 @@
+/*
+ * 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';
+
+export function getFieldTypeName(type: string) {
+ switch (type) {
+ case 'boolean':
+ return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', {
+ defaultMessage: 'Boolean field',
+ });
+ case 'conflict':
+ return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', {
+ defaultMessage: 'Conflicting field',
+ });
+ case 'date':
+ return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', {
+ defaultMessage: 'Date field',
+ });
+ case 'geo_point':
+ return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', {
+ defaultMessage: 'Geo point field',
+ });
+ case 'geo_shape':
+ return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', {
+ defaultMessage: 'Geo shape field',
+ });
+ case 'ip':
+ return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', {
+ defaultMessage: 'IP address field',
+ });
+ case 'murmur3':
+ return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', {
+ defaultMessage: 'Murmur3 field',
+ });
+ case 'number':
+ return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', {
+ defaultMessage: 'Number field',
+ });
+ case 'source':
+ // Note that this type is currently not provided, type for _source is undefined
+ return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', {
+ defaultMessage: 'Source field',
+ });
+ case 'string':
+ return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', {
+ defaultMessage: 'String field',
+ });
+ case 'nested':
+ return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', {
+ defaultMessage: 'Nested field',
+ });
+ default:
+ return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', {
+ defaultMessage: 'Unknown field',
+ });
+ }
+}
diff --git a/src/plugins/discover/public/application/components/help_menu/help_menu_util.js b/src/plugins/discover_legacy/public/application/components/help_menu/help_menu_util.js
similarity index 100%
rename from src/plugins/discover/public/application/components/help_menu/help_menu_util.js
rename to src/plugins/discover_legacy/public/application/components/help_menu/help_menu_util.js
diff --git a/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx
new file mode 100644
index 000000000000..998ababbc47f
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.test.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ReactWrapper } from 'enzyme';
+import { HitsCounter, HitsCounterProps } from './hits_counter';
+import { findTestSubject } from 'test_utils/helpers';
+
+describe('hits counter', function () {
+ let props: HitsCounterProps;
+ let component: ReactWrapper;
+
+ beforeAll(() => {
+ props = {
+ onResetQuery: jest.fn(),
+ showResetButton: true,
+ hits: 2,
+ };
+ });
+
+ it('HitsCounter renders a button by providing the showResetButton property', () => {
+ component = mountWithIntl();
+ expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1);
+ });
+
+ it('HitsCounter not renders a button when the showResetButton property is false', () => {
+ component = mountWithIntl(
+
+ );
+ expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0);
+ });
+
+ it('expect to render the number of hits', function () {
+ component = mountWithIntl();
+ const hits = findTestSubject(component, 'discoverQueryHits');
+ expect(hits.text()).toBe('2');
+ });
+
+ it('expect to render 1,899 hits if 1899 hits given', function () {
+ component = mountWithIntl(
+
+ );
+ const hits = findTestSubject(component, 'discoverQueryHits');
+ expect(hits.text()).toBe('1,899');
+ });
+
+ it('should reset query', function () {
+ component = mountWithIntl();
+ findTestSubject(component, 'resetSavedSearch').simulate('click');
+ expect(props.onResetQuery).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx
rename to src/plugins/discover_legacy/public/application/components/hits_counter/hits_counter.tsx
diff --git a/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts b/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts
new file mode 100644
index 000000000000..213cf96e0cc8
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/hits_counter/index.ts
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+export { HitsCounter } from './hits_counter';
diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap
new file mode 100644
index 000000000000..3897e22c50f1
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`returns the \`JsonCodeEditor\` component 1`] = `
+
+ {
+ "_index": "test",
+ "_type": "doc",
+ "_id": "foo",
+ "_score": 1,
+ "_source": {
+ "test": 123
+ }
+}
+
+`;
diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx
new file mode 100644
index 000000000000..2cb700b4d2ac
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.test.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 from 'react';
+import { shallow } from 'enzyme';
+import { JsonCodeBlock } from './json_code_block';
+import { IndexPattern } from '../../../../../data/public';
+
+it('returns the `JsonCodeEditor` component', () => {
+ const props = {
+ hit: { _index: 'test', _type: 'doc', _id: 'foo', _score: 1, _source: { test: 123 } },
+ columns: [],
+ indexPattern: {} as IndexPattern,
+ filter: jest.fn(),
+ onAddColumn: jest.fn(),
+ onRemoveColumn: jest.fn(),
+ };
+ expect(shallow()).toMatchSnapshot();
+});
diff --git a/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx
new file mode 100644
index 000000000000..f33cae438cb2
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/json_code_block/json_code_block.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 from 'react';
+import { EuiCodeBlock } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+export function JsonCodeBlock({ hit }: DocViewRenderProps) {
+ const label = i18n.translate('discover.docViews.json.codeEditorAriaLabel', {
+ defaultMessage: 'Read only JSON view of an opensearch document',
+ });
+ return (
+
+ {JSON.stringify(hit, null, 2)}
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx
new file mode 100644
index 000000000000..fbc98e2550e0
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.test.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ReactWrapper } from 'enzyme';
+import { LoadingSpinner } from './loading_spinner';
+import { findTestSubject } from 'test_utils/helpers';
+
+describe('loading spinner', function () {
+ let component: ReactWrapper;
+
+ it('LoadingSpinner renders a Searching text and a spinner', () => {
+ component = mountWithIntl();
+ expect(findTestSubject(component, 'loadingSpinnerText').text()).toBe('Searching');
+ expect(findTestSubject(component, 'loadingSpinner').length).toBe(1);
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx
new file mode 100644
index 000000000000..697c7a136d60
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/loading_spinner/loading_spinner.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 from 'react';
+import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@osd/i18n/react';
+
+export function LoadingSpinner() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap b/src/plugins/discover_legacy/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap
rename to src/plugins/discover_legacy/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap
diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/change_indexpattern.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx
rename to src/plugins/discover_legacy/public/application/components/sidebar/change_indexpattern.tsx
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss
new file mode 100644
index 000000000000..8e1dd41f66ab
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.scss
@@ -0,0 +1,4 @@
+.dscSidebarItem__fieldPopoverPanel {
+ min-width: 260px;
+ max-width: 300px;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx
new file mode 100644
index 000000000000..1b384a4b5550
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.test.tsx
@@ -0,0 +1,152 @@
+/*
+ * 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 from 'react';
+// @ts-ignore
+import { findTestSubject } from '@elastic/eui/lib/test';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { DiscoverField } from './discover_field';
+import { coreMock } from '../../../../../../core/public/mocks';
+import { IndexPatternField } from '../../../../../data/public';
+import { getStubIndexPattern } from '../../../../../data/public/test_utils';
+
+jest.mock('../../../opensearch_dashboards_services', () => ({
+ getServices: () => ({
+ history: () => ({
+ location: {
+ search: '',
+ },
+ }),
+ capabilities: {
+ visualize: {
+ show: true,
+ },
+ },
+ uiSettings: {
+ get: (key: string) => {
+ if (key === 'fields:popularLimit') {
+ return 5;
+ } else if (key === 'shortDots:enable') {
+ return false;
+ }
+ },
+ },
+ }),
+}));
+
+function getComponent({
+ selected = false,
+ showDetails = false,
+ useShortDots = false,
+ field,
+}: {
+ selected?: boolean;
+ showDetails?: boolean;
+ useShortDots?: boolean;
+ field?: IndexPatternField;
+}) {
+ const indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+ );
+
+ const finalField =
+ field ??
+ new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+
+ const props = {
+ indexPattern,
+ columns: [],
+ field: finalField,
+ getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 1 })),
+ onAddFilter: jest.fn(),
+ onAddField: jest.fn(),
+ onRemoveField: jest.fn(),
+ showDetails,
+ selected,
+ useShortDots,
+ };
+ const comp = mountWithIntl();
+ return { comp, props };
+}
+
+describe('discover sidebar field', function () {
+ it('should allow selecting fields', function () {
+ const { comp, props } = getComponent({});
+ findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
+ expect(props.onAddField).toHaveBeenCalledWith('bytes');
+ });
+ it('should allow deselecting fields', function () {
+ const { comp, props } = getComponent({ selected: true });
+ findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
+ expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
+ });
+ it('should trigger getDetails', function () {
+ const { comp, props } = getComponent({ selected: true });
+ findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
+ expect(props.getDetails).toHaveBeenCalledWith(props.field);
+ });
+ it('should not allow clicking on _source', function () {
+ const field = new IndexPatternField(
+ {
+ name: '_source',
+ type: '_source',
+ esTypes: ['_source'],
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ '_source'
+ );
+ const { comp, props } = getComponent({
+ selected: true,
+ field,
+ });
+ findTestSubject(comp, 'field-_source-showDetails').simulate('click');
+ expect(props.getDetails).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx
new file mode 100644
index 000000000000..e807267435eb
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field.tsx
@@ -0,0 +1,245 @@
+/*
+ * 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 } from 'react';
+import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { DiscoverFieldDetails } from './discover_field_details';
+import { FieldIcon, FieldButton } from '../../../../../opensearch_dashboards_react/public';
+import { FieldDetails } from './types';
+import { IndexPatternField, IndexPattern } from '../../../../../data/public';
+import { shortenDottedString } from '../../helpers';
+import { getFieldTypeName } from './lib/get_field_type_name';
+import './discover_field.scss';
+
+export interface DiscoverFieldProps {
+ /**
+ * the selected columns displayed in the doc table in discover
+ */
+ columns: string[];
+ /**
+ * The displayed field
+ */
+ field: IndexPatternField;
+ /**
+ * The currently selected index pattern
+ */
+ indexPattern: IndexPattern;
+ /**
+ * Callback to add/select the field
+ */
+ onAddField: (fieldName: string) => void;
+ /**
+ * Callback to add a filter to filter bar
+ */
+ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+ /**
+ * Callback to remove/deselect a the field
+ * @param fieldName
+ */
+ onRemoveField: (fieldName: string) => void;
+ /**
+ * Retrieve details data for the field
+ */
+ getDetails: (field: IndexPatternField) => FieldDetails;
+ /**
+ * Determines whether the field is selected
+ */
+ selected?: boolean;
+ /**
+ * Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2
+ */
+ useShortDots?: boolean;
+}
+
+export function DiscoverField({
+ columns,
+ field,
+ indexPattern,
+ onAddField,
+ onRemoveField,
+ onAddFilter,
+ getDetails,
+ selected,
+ useShortDots,
+}: DiscoverFieldProps) {
+ const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
+ defaultMessage: 'Add {field} to table',
+ values: { field: field.name },
+ });
+ const removeLabelAria = i18n.translate(
+ 'discover.fieldChooser.discoverField.removeButtonAriaLabel',
+ {
+ defaultMessage: 'Remove {field} from table',
+ values: { field: field.name },
+ }
+ );
+
+ const [infoIsOpen, setOpen] = useState(false);
+
+ const toggleDisplay = (f: IndexPatternField) => {
+ if (selected) {
+ onRemoveField(f.name);
+ } else {
+ onAddField(f.name);
+ }
+ };
+
+ function togglePopover() {
+ setOpen(!infoIsOpen);
+ }
+
+ function wrapOnDot(str?: string) {
+ // u200B is a non-width white-space character, which allows
+ // the browser to efficiently word-wrap right after the dot
+ // without us having to draw a lot of extra DOM elements, etc
+ return str ? str.replace(/\./g, '.\u200B') : '';
+ }
+
+ const dscFieldIcon = (
+
+ );
+
+ const fieldName = (
+
+ {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)}
+
+ );
+
+ let actionButton;
+ if (field.name !== '_source' && !selected) {
+ actionButton = (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={addLabelAria}
+ />
+
+ );
+ } else if (field.name !== '_source' && selected) {
+ actionButton = (
+
+ ) => {
+ if (ev.type === 'click') {
+ ev.currentTarget.focus();
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ toggleDisplay(field);
+ }}
+ data-test-subj={`fieldToggle-${field.name}`}
+ aria-label={removeLabelAria}
+ />
+
+ );
+ }
+
+ if (field.type === '_source') {
+ return (
+
+ );
+ }
+
+ return (
+ {
+ togglePopover();
+ }}
+ dataTestSubj={`field-${field.name}-showDetails`}
+ fieldIcon={dscFieldIcon}
+ fieldAction={actionButton}
+ fieldName={fieldName}
+ />
+ }
+ isOpen={infoIsOpen}
+ closePopover={() => setOpen(false)}
+ anchorPosition="rightUp"
+ panelClassName="dscSidebarItem__fieldPopoverPanel"
+ >
+
+ {' '}
+ {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
+ defaultMessage: 'Top 5 values',
+ })}
+
+ {infoIsOpen && (
+
+ )}
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss
new file mode 100644
index 000000000000..90b645f70084
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.scss
@@ -0,0 +1,4 @@
+.dscFieldDetails__barContainer {
+ // Constrains value to the flex item, and allows for truncation when necessary
+ min-width: 0;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx
new file mode 100644
index 000000000000..6a4dbe295e50
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_bucket.tsx
@@ -0,0 +1,133 @@
+/*
+ * 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 from 'react';
+import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import { StringFieldProgressBar } from './string_progress_bar';
+import { Bucket } from './types';
+import { IndexPatternField } from '../../../../../data/public';
+import './discover_field_bucket.scss';
+
+interface Props {
+ bucket: Bucket;
+ field: IndexPatternField;
+ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+
+export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
+ const emptyTxt = i18n.translate('discover.fieldChooser.detailViews.emptyStringText', {
+ defaultMessage: 'Empty string',
+ });
+ const addLabel = i18n.translate('discover.fieldChooser.detailViews.filterValueButtonAriaLabel', {
+ defaultMessage: 'Filter for {field}: "{value}"',
+ values: { value: bucket.value, field: field.name },
+ });
+ const removeLabel = i18n.translate(
+ 'discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel',
+ {
+ defaultMessage: 'Filter out {field}: "{value}"',
+ values: { value: bucket.value, field: field.name },
+ }
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {bucket.display === '' ? emptyTxt : bucket.display}
+
+
+
+
+ {bucket.percent.toFixed(1)}%
+
+
+
+
+
+ {field.filterable && (
+
+
+ onAddFilter(field, bucket.value, '+')}
+ aria-label={addLabel}
+ data-test-subj={`plus-${field.name}-${bucket.value}`}
+ style={{
+ minHeight: 'auto',
+ minWidth: 'auto',
+ paddingRight: 2,
+ paddingLeft: 2,
+ paddingTop: 0,
+ paddingBottom: 0,
+ }}
+ className={'euiButtonIcon--auto'}
+ />
+ onAddFilter(field, bucket.value, '-')}
+ aria-label={removeLabel}
+ data-test-subj={`minus-${field.name}-${bucket.value}`}
+ style={{
+ minHeight: 'auto',
+ minWidth: 'auto',
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingRight: 2,
+ paddingLeft: 2,
+ }}
+ className={'euiButtonIcon--auto'}
+ />
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.scss
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/discover_field_details.scss
rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.scss
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx
new file mode 100644
index 000000000000..63d5c7ace303
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.test.tsx
@@ -0,0 +1,312 @@
+/*
+ * 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 from 'react';
+// @ts-ignore
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { act } from '@testing-library/react';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
+import { DiscoverFieldDetails } from './discover_field_details';
+import { coreMock } from '../../../../../../core/public/mocks';
+import { IndexPatternField } from '../../../../../data/public';
+import { getStubIndexPattern } from '../../../../../data/public/test_utils';
+
+const mockGetHref = jest.fn();
+const mockGetTriggerCompatibleActions = jest.fn();
+
+jest.mock('../../../opensearch_dashboards_services', () => ({
+ getUiActions: () => ({
+ getTriggerCompatibleActions: mockGetTriggerCompatibleActions,
+ }),
+}));
+
+const indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+);
+
+describe('discover sidebar field details', function () {
+ const defaultProps = {
+ columns: [],
+ details: { buckets: [], error: '', exists: 1, total: 1 },
+ indexPattern,
+ onAddFilter: jest.fn(),
+ };
+
+ beforeEach(() => {
+ mockGetHref.mockReturnValue('/foo/bar');
+ mockGetTriggerCompatibleActions.mockReturnValue([
+ {
+ getHref: mockGetHref,
+ },
+ ]);
+ });
+
+ function mountComponent(field: IndexPatternField, props?: Record) {
+ const compProps = { ...defaultProps, ...props, field };
+ return mountWithIntl();
+ }
+
+ it('should render buckets if they exist', async function () {
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const buckets = [1, 2, 3].map((n) => ({
+ display: `display-${n}`,
+ value: `value-${n}`,
+ percent: 25,
+ count: 100,
+ }));
+ const comp = mountComponent(visualizableField, {
+ details: { ...defaultProps.details, buckets },
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe(
+ buckets.length
+ );
+ // Visualize link should not be rendered until async hook update
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0);
+
+ // Complete async hook
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').children().length).toBe(
+ buckets.length
+ );
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1);
+ });
+
+ it('should only render buckets if they exist', async function () {
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const comp = mountComponent(visualizableField);
+ expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+
+ expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1);
+ });
+
+ it('should render a details error', async function () {
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const errText = 'Some error';
+ const comp = mountComponent(visualizableField, {
+ details: { ...defaultProps.details, error: errText },
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeContainer').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeBucketContainer').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualizeError').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualizeError').text()).toBe(errText);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1);
+ });
+
+ it('should handle promise rejection from isFieldVisualizable', async function () {
+ mockGetTriggerCompatibleActions.mockRejectedValue(new Error('Async error'));
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const comp = mountComponent(visualizableField);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0);
+ });
+
+ it('should handle promise rejection from getVisualizeHref', async function () {
+ mockGetHref.mockRejectedValue(new Error('Async error'));
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const comp = mountComponent(visualizableField);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(0);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(0);
+ });
+
+ it('should enable the visualize link for a number field', async function () {
+ const visualizableField = new IndexPatternField(
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'bytes'
+ );
+ const comp = mountComponent(visualizableField);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualizeLink').length).toBe(1);
+ expect(findTestSubject(comp, 'fieldVisualize-bytes').length).toBe(1);
+ });
+
+ it('should disable the visualize link for an _id field', async function () {
+ expect.assertions(1);
+ const conflictField = new IndexPatternField(
+ {
+ name: '_id',
+ type: 'string',
+ esTypes: ['_id'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'test'
+ );
+ const comp = mountComponent(conflictField);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualize-_id').length).toBe(0);
+ });
+
+ it('should disable the visualize link for an unknown field', async function () {
+ const unknownField = new IndexPatternField(
+ {
+ name: 'test',
+ type: 'unknown',
+ esTypes: ['double'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ 'test'
+ );
+ const comp = mountComponent(unknownField);
+
+ await act(async () => {
+ await nextTick();
+ comp.update();
+ });
+ expect(findTestSubject(comp, 'fieldVisualize-test').length).toBe(0);
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx
new file mode 100644
index 000000000000..906c173ed07d
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_details.tsx
@@ -0,0 +1,153 @@
+/*
+ * 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, useEffect } from 'react';
+import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@osd/i18n/react';
+import { DiscoverFieldBucket } from './discover_field_bucket';
+import { getWarnings } from './lib/get_warnings';
+import {
+ triggerVisualizeActions,
+ isFieldVisualizable,
+ getVisualizeHref,
+} from './lib/visualize_trigger_utils';
+import { Bucket, FieldDetails } from './types';
+import { IndexPatternField, IndexPattern } from '../../../../../data/public';
+import './discover_field_details.scss';
+
+interface DiscoverFieldDetailsProps {
+ columns: string[];
+ details: FieldDetails;
+ field: IndexPatternField;
+ indexPattern: IndexPattern;
+ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+}
+
+export function DiscoverFieldDetails({
+ columns,
+ details,
+ field,
+ indexPattern,
+ onAddFilter,
+}: DiscoverFieldDetailsProps) {
+ const warnings = getWarnings(field);
+ const [showVisualizeLink, setShowVisualizeLink] = useState(false);
+ const [visualizeLink, setVisualizeLink] = useState('');
+
+ useEffect(() => {
+ const checkIfVisualizable = async () => {
+ const visualizable = await isFieldVisualizable(field, indexPattern.id, columns).catch(
+ () => false
+ );
+
+ setShowVisualizeLink(visualizable);
+ if (visualizable) {
+ const href = await getVisualizeHref(field, indexPattern.id, columns).catch(() => '');
+ setVisualizeLink(href || '');
+ }
+ };
+ checkIfVisualizable();
+ }, [field, indexPattern.id, columns]);
+
+ const handleVisualizeLinkClick = (event: React.MouseEvent) => {
+ // regular link click. let the uiActions code handle the navigation and show popup if needed
+ event.preventDefault();
+ triggerVisualizeActions(field, indexPattern.id, columns);
+ };
+
+ return (
+ <>
+
+ {details.error && (
+
+ {details.error}
+
+ )}
+
+ {!details.error && details.buckets.length > 0 && (
+
+ {details.buckets.map((bucket: Bucket, idx: number) => (
+
+ ))}
+
+ )}
+
+ {showVisualizeLink && visualizeLink && (
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+ handleVisualizeLinkClick(e)}
+ href={visualizeLink}
+ size="s"
+ className="dscFieldDetails__visualizeBtn"
+ data-test-subj={`fieldVisualize-${field.name}`}
+ >
+
+
+ {warnings.length > 0 && (
+
+ )}
+
+ )}
+
+ {!details.error && (
+
+
+ {!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
+ onAddFilter('_exists_', field.name, '+')}>
+ {' '}
+ {details.exists}
+
+ ) : (
+ {details.exists}
+ )}{' '}
+ / {details.total}{' '}
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx
new file mode 100644
index 000000000000..f78505e11f1e
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.test.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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 from 'react';
+import { act } from 'react-dom/test-utils';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { findTestSubject } from 'test_utils/helpers';
+import { DiscoverFieldSearch, Props } from './discover_field_search';
+import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui';
+import { ReactWrapper } from 'enzyme';
+
+describe('DiscoverFieldSearch', () => {
+ const defaultProps = {
+ onChange: jest.fn(),
+ value: 'test',
+ types: ['any', 'string', '_source'],
+ };
+
+ function mountComponent(props?: Props) {
+ const compProps = props || defaultProps;
+ return mountWithIntl();
+ }
+
+ function findButtonGroup(component: ReactWrapper, id: string) {
+ return component.find(`[data-test-subj="${id}ButtonGroup"]`).first();
+ }
+
+ test('enter value', () => {
+ const component = mountComponent();
+ const input = findTestSubject(component, 'fieldFilterSearchInput');
+ input.simulate('change', { target: { value: 'new filter' } });
+ expect(defaultProps.onChange).toBeCalledTimes(1);
+ });
+
+ test('change in active filters should change facet selection and call onChange', () => {
+ const onChange = jest.fn();
+ const component = mountComponent({ ...defaultProps, ...{ onChange } });
+ let btn = findTestSubject(component, 'toggleFieldFilterButton');
+ expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy();
+ btn.simulate('click');
+ const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable');
+ act(() => {
+ // @ts-ignore
+ (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
+ });
+ component.update();
+ btn = findTestSubject(component, 'toggleFieldFilterButton');
+ expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true);
+ expect(onChange).toBeCalledWith('aggregatable', true);
+ });
+
+ test('change in active filters should change filters count', () => {
+ const component = mountComponent();
+ let btn = findTestSubject(component, 'toggleFieldFilterButton');
+ btn.simulate('click');
+ btn = findTestSubject(component, 'toggleFieldFilterButton');
+ const badge = btn.find('.euiNotificationBadge');
+ // no active filters
+ expect(badge.text()).toEqual('0');
+ // change value of aggregatable select
+ const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable');
+ act(() => {
+ // @ts-ignore
+ (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
+ });
+ component.update();
+ expect(badge.text()).toEqual('1');
+ // change value of searchable select
+ const searchableButtonGroup = findButtonGroup(component, 'searchable');
+ act(() => {
+ // @ts-ignore
+ (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null);
+ });
+ component.update();
+ expect(badge.text()).toEqual('2');
+ // change value of searchable select
+ act(() => {
+ // @ts-ignore
+ (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null);
+ });
+ component.update();
+ expect(badge.text()).toEqual('1');
+ });
+
+ test('change in missing fields switch should not change filter count', () => {
+ const component = mountComponent();
+ const btn = findTestSubject(component, 'toggleFieldFilterButton');
+ btn.simulate('click');
+ const badge = btn.find('.euiNotificationBadge');
+ expect(badge.text()).toEqual('0');
+ const missingSwitch = findTestSubject(component, 'missingSwitch');
+ missingSwitch.simulate('change', { target: { value: false } });
+ expect(badge.text()).toEqual('0');
+ });
+
+ test('change in filters triggers onChange', () => {
+ const onChange = jest.fn();
+ const component = mountComponent({ ...defaultProps, ...{ onChange } });
+ const btn = findTestSubject(component, 'toggleFieldFilterButton');
+ btn.simulate('click');
+ const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable');
+ const missingSwitch = findTestSubject(component, 'missingSwitch');
+ act(() => {
+ // @ts-ignore
+ (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null);
+ });
+ missingSwitch.simulate('click');
+ expect(onChange).toBeCalledTimes(2);
+ });
+
+ test('change in type filters triggers onChange with appropriate value', () => {
+ const onChange = jest.fn();
+ const component = mountComponent({ ...defaultProps, ...{ onChange } });
+ const btn = findTestSubject(component, 'toggleFieldFilterButton');
+ btn.simulate('click');
+ const typeSelector = findTestSubject(component, 'typeSelect');
+ typeSelector.simulate('change', { target: { value: 'string' } });
+ expect(onChange).toBeCalledWith('type', 'string');
+ typeSelector.simulate('change', { target: { value: 'any' } });
+ expect(onChange).toBeCalledWith('type', 'any');
+ });
+
+ test('click on filter button should open and close popover', () => {
+ const component = mountComponent();
+ const btn = findTestSubject(component, 'toggleFieldFilterButton');
+ btn.simulate('click');
+ let popover = component.find(EuiPopover);
+ expect(popover.prop('isOpen')).toBe(true);
+ btn.simulate('click');
+ popover = component.find(EuiPopover);
+ expect(popover.prop('isOpen')).toBe(false);
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx
new file mode 100644
index 000000000000..4a1390cb1955
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_field_search.tsx
@@ -0,0 +1,313 @@
+/*
+ * 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, { OptionHTMLAttributes, ReactNode, useState } from 'react';
+import { i18n } from '@osd/i18n';
+import {
+ EuiFacetButton,
+ EuiFieldSearch,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiPopover,
+ EuiPopoverFooter,
+ EuiPopoverTitle,
+ EuiSelect,
+ EuiSwitch,
+ EuiSwitchEvent,
+ EuiForm,
+ EuiFormRow,
+ EuiButtonGroup,
+ EuiOutsideClickDetector,
+} from '@elastic/eui';
+import { FormattedMessage } from '@osd/i18n/react';
+
+export interface State {
+ searchable: string;
+ aggregatable: string;
+ type: string;
+ missing: boolean;
+ [index: string]: string | boolean;
+}
+
+export interface Props {
+ /**
+ * triggered on input of user into search field
+ */
+ onChange: (field: string, value: string | boolean | undefined) => void;
+
+ /**
+ * the input value of the user
+ */
+ value?: string;
+
+ /**
+ * types for the type filter
+ */
+ types: string[];
+}
+
+/**
+ * Component is Discover's side bar to search of available fields
+ * Additionally there's a button displayed that allows the user to show/hide more filter fields
+ */
+export function DiscoverFieldSearch({ onChange, value, types }: Props) {
+ const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', {
+ defaultMessage: 'Search field names',
+ });
+ const aggregatableLabel = i18n.translate('discover.fieldChooser.filter.aggregatableLabel', {
+ defaultMessage: 'Aggregatable',
+ });
+ const searchableLabel = i18n.translate('discover.fieldChooser.filter.searchableLabel', {
+ defaultMessage: 'Searchable',
+ });
+ const typeLabel = i18n.translate('discover.fieldChooser.filter.typeLabel', {
+ defaultMessage: 'Type',
+ });
+ const typeOptions = types
+ ? types.map((type) => {
+ return { value: type, text: type };
+ })
+ : [{ value: 'any', text: 'any' }];
+
+ const [activeFiltersCount, setActiveFiltersCount] = useState(0);
+ const [isPopoverOpen, setPopoverOpen] = useState(false);
+ const [values, setValues] = useState({
+ searchable: 'any',
+ aggregatable: 'any',
+ type: 'any',
+ missing: true,
+ });
+
+ if (typeof value !== 'string') {
+ // at initial rendering value is undefined (angular related), this catches the warning
+ // should be removed once all is react
+ return null;
+ }
+
+ const filterBtnAriaLabel = isPopoverOpen
+ ? i18n.translate('discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', {
+ defaultMessage: 'Hide field filter settings',
+ })
+ : i18n.translate('discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', {
+ defaultMessage: 'Show field filter settings',
+ });
+
+ const handleFacetButtonClicked = () => {
+ setPopoverOpen(!isPopoverOpen);
+ };
+
+ const applyFilterValue = (id: string, filterValue: string | boolean) => {
+ switch (filterValue) {
+ case 'any':
+ if (id !== 'type') {
+ onChange(id, undefined);
+ } else {
+ onChange(id, filterValue);
+ }
+ break;
+ case 'true':
+ onChange(id, true);
+ break;
+ case 'false':
+ onChange(id, false);
+ break;
+ default:
+ onChange(id, filterValue);
+ }
+ };
+
+ const isFilterActive = (name: string, filterValue: string | boolean) => {
+ return name !== 'missing' && filterValue !== 'any';
+ };
+
+ const handleValueChange = (name: string, filterValue: string | boolean) => {
+ const previousValue = values[name];
+ updateFilterCount(name, previousValue, filterValue);
+ const updatedValues = { ...values };
+ updatedValues[name] = filterValue;
+ setValues(updatedValues);
+ applyFilterValue(name, filterValue);
+ };
+
+ const updateFilterCount = (
+ name: string,
+ previousValue: string | boolean,
+ currentValue: string | boolean
+ ) => {
+ const previouslyFilterActive = isFilterActive(name, previousValue);
+ const filterActive = isFilterActive(name, currentValue);
+ const diff = Number(filterActive) - Number(previouslyFilterActive);
+ setActiveFiltersCount(activeFiltersCount + diff);
+ };
+
+ const handleMissingChange = (e: EuiSwitchEvent) => {
+ const missingValue = e.target.checked;
+ handleValueChange('missing', missingValue);
+ };
+
+ const buttonContent = (
+ }
+ isSelected={activeFiltersCount > 0}
+ quantity={activeFiltersCount}
+ onClick={handleFacetButtonClicked}
+ >
+
+
+ );
+
+ const select = (
+ id: string,
+ selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>,
+ selectValue: string
+ ) => {
+ return (
+ ) =>
+ handleValueChange(id, e.target.value)
+ }
+ aria-label={i18n.translate('discover.fieldChooser.filter.fieldSelectorLabel', {
+ defaultMessage: 'Selection of {id} filter options',
+ values: { id },
+ })}
+ data-test-subj={`${id}Select`}
+ compressed
+ />
+ );
+ };
+
+ const toggleButtons = (id: string) => {
+ return [
+ {
+ id: `${id}-any`,
+ label: 'any',
+ },
+ {
+ id: `${id}-true`,
+ label: 'yes',
+ },
+ {
+ id: `${id}-false`,
+ label: 'no',
+ },
+ ];
+ };
+
+ const buttonGroup = (id: string, legend: string) => {
+ return (
+ handleValueChange(id, optionId.replace(`${id}-`, ''))}
+ buttonSize="compressed"
+ isFullWidth
+ data-test-subj={`${id}ButtonGroup`}
+ />
+ );
+ };
+
+ const selectionPanel = (
+
+
+
+ {buttonGroup('aggregatable', aggregatableLabel)}
+
+
+ {buttonGroup('searchable', searchableLabel)}
+
+
+ {select('type', typeOptions, values.type)}
+
+
+
+ );
+
+ return (
+
+
+
+ onChange('name', event.currentTarget.value)}
+ placeholder={searchPlaceholder}
+ value={value}
+ />
+
+
+
+ {}} isDisabled={!isPopoverOpen}>
+ {
+ setPopoverOpen(false);
+ }}
+ button={buttonContent}
+ >
+
+ {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
+ defaultMessage: 'Filter by type',
+ })}
+
+ {selectionPanel}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.test.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx
rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.test.tsx
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx
rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern.tsx
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern_title.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx
rename to src/plugins/discover_legacy/public/application/components/sidebar/discover_index_pattern_title.tsx
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss
new file mode 100644
index 000000000000..9c80e0afa600
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.scss
@@ -0,0 +1,99 @@
+.dscSidebar__container {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ background-color: transparent;
+ border-right-color: transparent;
+ border-bottom-color: transparent;
+}
+
+.dscIndexPattern__container {
+ display: flex;
+ align-items: center;
+ height: $euiSize * 3;
+ margin-top: -$euiSizeS;
+}
+
+.dscIndexPattern__triggerButton {
+ @include euiTitle("xs");
+
+ line-height: $euiSizeXXL;
+}
+
+.dscFieldList {
+ list-style: none;
+ margin-bottom: 0;
+}
+
+.dscFieldListHeader {
+ padding: $euiSizeS $euiSizeS 0 $euiSizeS;
+ background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
+}
+
+.dscFieldList--popular {
+ background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
+}
+
+.dscFieldChooser {
+ padding-left: $euiSize;
+}
+
+.dscFieldChooser__toggle {
+ color: $euiColorMediumShade;
+ margin-left: $euiSizeS !important;
+}
+
+.dscSidebarItem {
+ &:hover,
+ &:focus-within,
+ &[class*="-isActive"] {
+ .dscSidebarItem__action {
+ opacity: 1;
+ }
+ }
+}
+
+/**
+ * 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 {
+ opacity: 0; /* 1 */
+ transition: none;
+
+ &:focus {
+ opacity: 1; /* 2 */
+ }
+
+ font-size: $euiFontSizeXS;
+ padding: 2px 6px !important;
+ height: 22px !important;
+ min-width: auto !important;
+
+ .euiButton__content {
+ padding: 0 4px;
+ }
+}
+
+.dscFieldSearch {
+ padding: $euiSizeS;
+}
+
+.dscFieldSearch__toggleButton {
+ width: calc(100% - #{$euiSizeS});
+ color: $euiColorPrimary;
+ padding-left: $euiSizeXS;
+ margin-left: $euiSizeXS;
+}
+
+.dscFieldSearch__filterWrapper {
+ flex-grow: 0;
+}
+
+.dscFieldSearch__formWrapper {
+ padding: $euiSizeM;
+}
+
+.dscFieldDetails {
+ color: $euiTextColor;
+ margin-bottom: $euiSizeS;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx
new file mode 100644
index 000000000000..fa692ca22b5b
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.test.tsx
@@ -0,0 +1,148 @@
+/*
+ * 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 _ from 'lodash';
+import { ReactWrapper } from 'enzyme';
+import { findTestSubject } from 'test_utils/helpers';
+// @ts-ignore
+import realHits from 'fixtures/real_hits.js';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import React from 'react';
+import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar';
+import { coreMock } from '../../../../../../core/public/mocks';
+import { IndexPatternAttributes } from '../../../../../data/common';
+import { getStubIndexPattern } from '../../../../../data/public/test_utils';
+import { SavedObject } from '../../../../../../core/types';
+
+jest.mock('../../../opensearch_dashboards_services', () => ({
+ getServices: () => ({
+ history: () => ({
+ location: {
+ search: '',
+ },
+ }),
+ capabilities: {
+ visualize: {
+ show: true,
+ },
+ discover: {
+ save: false,
+ },
+ },
+ uiSettings: {
+ get: (key: string) => {
+ if (key === 'fields:popularLimit') {
+ return 5;
+ } else if (key === 'shortDots:enable') {
+ return false;
+ }
+ },
+ },
+ }),
+}));
+
+jest.mock('./lib/get_index_pattern_field_list', () => ({
+ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields),
+}));
+
+function getCompProps() {
+ const indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+ );
+
+ // @ts-expect-error _.each() is passing additional args to flattenHit
+ const hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit) as Array<
+ Record
+ >;
+
+ const indexPatternList = [
+ { id: '0', attributes: { title: 'b' } } as SavedObject,
+ { id: '1', attributes: { title: 'a' } } as SavedObject,
+ { id: '2', attributes: { title: 'c' } } as SavedObject,
+ ];
+
+ const fieldCounts: Record = {};
+
+ for (const hit of hits) {
+ for (const key of Object.keys(indexPattern.flattenHit(hit))) {
+ fieldCounts[key] = (fieldCounts[key] || 0) + 1;
+ }
+ }
+ return {
+ columns: ['extension'],
+ fieldCounts,
+ hits,
+ indexPatternList,
+ onAddFilter: jest.fn(),
+ onAddField: jest.fn(),
+ onRemoveField: jest.fn(),
+ selectedIndexPattern: indexPattern,
+ setIndexPattern: jest.fn(),
+ state: {},
+ };
+}
+
+describe('discover sidebar', function () {
+ let props: DiscoverSidebarProps;
+ let comp: ReactWrapper;
+
+ beforeAll(() => {
+ props = getCompProps();
+ comp = mountWithIntl();
+ });
+
+ it('should have Selected Fields and Available Fields with Popular Fields sections', function () {
+ const popular = findTestSubject(comp, 'fieldList-popular');
+ const selected = findTestSubject(comp, 'fieldList-selected');
+ const unpopular = findTestSubject(comp, 'fieldList-unpopular');
+ expect(popular.children().length).toBe(1);
+ expect(unpopular.children().length).toBe(7);
+ expect(selected.children().length).toBe(1);
+ });
+ it('should allow selecting fields', function () {
+ findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
+ expect(props.onAddField).toHaveBeenCalledWith('bytes');
+ });
+ it('should allow deselecting fields', function () {
+ findTestSubject(comp, 'fieldToggle-extension').simulate('click');
+ expect(props.onRemoveField).toHaveBeenCalledWith('extension');
+ });
+ it('should allow adding filters', function () {
+ findTestSubject(comp, 'field-extension-showDetails').simulate('click');
+ findTestSubject(comp, 'plus-extension-gif').simulate('click');
+ expect(props.onAddFilter).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx
new file mode 100644
index 000000000000..865aff590286
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/discover_sidebar.tsx
@@ -0,0 +1,326 @@
+/*
+ * 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 './discover_sidebar.scss';
+import React, { useCallback, useEffect, useState, useMemo } from 'react';
+import { i18n } from '@osd/i18n';
+import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui';
+import { sortBy } from 'lodash';
+import { FormattedMessage, I18nProvider } from '@osd/i18n/react';
+import { DiscoverField } from './discover_field';
+import { DiscoverIndexPattern } from './discover_index_pattern';
+import { DiscoverFieldSearch } from './discover_field_search';
+import { IndexPatternAttributes } from '../../../../../data/common';
+import { SavedObject } from '../../../../../../core/types';
+import { FIELDS_LIMIT_SETTING } from '../../../../common';
+import { groupFields } from './lib/group_fields';
+import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public';
+import { getDetails } from './lib/get_details';
+import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter';
+import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
+import { getServices } from '../../../opensearch_dashboards_services';
+
+export interface DiscoverSidebarProps {
+ /**
+ * the selected columns displayed in the doc table in discover
+ */
+ columns: string[];
+ /**
+ * a statistics of the distribution of fields in the given hits
+ */
+ fieldCounts: Record;
+ /**
+ * hits fetched from OpenSearch, displayed in the doc table
+ */
+ hits: Array>;
+ /**
+ * List of available index patterns
+ */
+ indexPatternList: Array>;
+ /**
+ * Callback function when selecting a field
+ */
+ onAddField: (fieldName: string) => void;
+ /**
+ * Callback function when adding a filter from sidebar
+ */
+ onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
+ /**
+ * Callback function when removing a field
+ * @param fieldName
+ */
+ onRemoveField: (fieldName: string) => void;
+ /**
+ * Currently selected index pattern
+ */
+ selectedIndexPattern?: IndexPattern;
+ /**
+ * Callback function to select another index pattern
+ */
+ setIndexPattern: (id: string) => void;
+}
+
+export function DiscoverSidebar({
+ columns,
+ fieldCounts,
+ hits,
+ indexPatternList,
+ onAddField,
+ onAddFilter,
+ onRemoveField,
+ selectedIndexPattern,
+ setIndexPattern,
+}: DiscoverSidebarProps) {
+ const [showFields, setShowFields] = useState(false);
+ const [fields, setFields] = useState(null);
+ const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter());
+ const services = useMemo(() => getServices(), []);
+
+ useEffect(() => {
+ const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
+ setFields(newFields);
+ }, [selectedIndexPattern, fieldCounts, hits, services]);
+
+ const onChangeFieldSearch = useCallback(
+ (field: string, value: string | boolean | undefined) => {
+ const newState = setFieldFilterProp(fieldFilterState, field, value);
+ setFieldFilterState(newState);
+ },
+ [fieldFilterState]
+ );
+
+ const getDetailsByField = useCallback(
+ (ipField: IndexPatternField) => getDetails(ipField, hits, selectedIndexPattern),
+ [hits, selectedIndexPattern]
+ );
+
+ const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING);
+ const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
+
+ const {
+ selected: selectedFields,
+ popular: popularFields,
+ unpopular: unpopularFields,
+ } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [
+ fields,
+ columns,
+ popularLimit,
+ fieldCounts,
+ fieldFilterState,
+ ]);
+
+ const fieldTypes = useMemo(() => {
+ const result = ['any'];
+ if (Array.isArray(fields)) {
+ for (const field of fields) {
+ if (result.indexOf(field.type) === -1) {
+ result.push(field.type);
+ }
+ }
+ }
+ return result;
+ }, [fields]);
+
+ if (!selectedIndexPattern || !fields) {
+ return null;
+ }
+
+ return (
+
+
+ o.attributes.title)}
+ />
+
+
+
+
+ {fields.length > 0 && (
+ <>
+
+
+
+
+
+
+
+ {selectedFields.map((field: IndexPatternField) => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ setShowFields(!showFields)}
+ aria-label={
+ showFields
+ ? i18n.translate(
+ 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel',
+ {
+ defaultMessage: 'Hide fields',
+ }
+ )
+ : i18n.translate(
+ 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel',
+ {
+ defaultMessage: 'Show fields',
+ }
+ )
+ }
+ />
+
+
+ >
+ )}
+ {popularFields.length > 0 && (
+
+
+
+
+
+
+
+ {popularFields.map((field: IndexPatternField) => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {unpopularFields.map((field: IndexPatternField) => {
+ return (
+ -
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/index.ts b/src/plugins/discover_legacy/public/application/components/sidebar/index.ts
new file mode 100644
index 000000000000..2799d47da83f
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/index.ts
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+export { DiscoverSidebar } from './discover_sidebar';
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts
new file mode 100644
index 000000000000..d580f7ae228a
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.test.ts
@@ -0,0 +1,268 @@
+/*
+ * 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 _ from 'lodash';
+// @ts-ignore
+import realHits from 'fixtures/real_hits.js';
+// @ts-ignore
+import stubbedLogstashFields from 'fixtures/logstash_fields';
+import { coreMock } from '../../../../../../../core/public/mocks';
+import { IndexPattern, IndexPatternField } from '../../../../../../data/public';
+import { getStubIndexPattern } from '../../../../../../data/public/test_utils';
+import {
+ groupValues,
+ getFieldValues,
+ getFieldValueCounts,
+ FieldValueCountsParams,
+} from './field_calculator';
+import { Bucket } from '../types';
+
+let indexPattern: IndexPattern;
+
+describe('field_calculator', function () {
+ beforeEach(function () {
+ indexPattern = getStubIndexPattern(
+ 'logstash-*',
+ (cfg: any) => cfg,
+ 'time',
+ stubbedLogstashFields(),
+ coreMock.createSetup()
+ );
+ });
+
+ describe('groupValues', function () {
+ let groups: Record;
+ let grouped: boolean;
+ let values: any[];
+ beforeEach(function () {
+ values = [
+ ['foo', 'bar'],
+ 'foo',
+ 'foo',
+ undefined,
+ ['foo', 'bar'],
+ 'bar',
+ 'baz',
+ null,
+ null,
+ null,
+ 'foo',
+ undefined,
+ ];
+ groups = groupValues(values, grouped);
+ });
+
+ it('should return an object values', function () {
+ expect(groups).toBeInstanceOf(Object);
+ });
+
+ it('should throw an error if any value is a plain object', function () {
+ expect(function () {
+ groupValues([{}, true, false], grouped);
+ }).toThrowError();
+ });
+
+ it('should handle values with dots in them', function () {
+ values = ['0', '0.........', '0.......,.....'];
+ groups = groupValues(values, grouped);
+ expect(groups[values[0]].count).toBe(1);
+ expect(groups[values[1]].count).toBe(1);
+ expect(groups[values[2]].count).toBe(1);
+ });
+
+ it('should have a key for value in the array when not grouping array terms', function () {
+ expect(_.keys(groups).length).toBe(3);
+ expect(groups.foo).toBeInstanceOf(Object);
+ expect(groups.bar).toBeInstanceOf(Object);
+ expect(groups.baz).toBeInstanceOf(Object);
+ });
+
+ it('should count array terms independently', function () {
+ expect(groups['foo,bar']).toBeUndefined();
+ expect(groups.foo.count).toBe(5);
+ expect(groups.bar.count).toBe(3);
+ expect(groups.baz.count).toBe(1);
+ });
+
+ describe('grouped array terms', function () {
+ beforeEach(function () {
+ grouped = true;
+ groups = groupValues(values, grouped);
+ });
+
+ it('should group array terms when grouped is true', function () {
+ expect(_.keys(groups).length).toBe(4);
+ expect(groups['foo,bar']).toBeInstanceOf(Object);
+ });
+
+ it('should contain the original array as the value', function () {
+ expect(groups['foo,bar'].value).toEqual(['foo', 'bar']);
+ });
+
+ it('should count the pairs separately from the values they contain', function () {
+ expect(groups['foo,bar'].count).toBe(2);
+ expect(groups.foo.count).toBe(3);
+ expect(groups.bar.count).toBe(1);
+ });
+ });
+ });
+
+ describe('getFieldValues', function () {
+ let hits: any;
+
+ beforeEach(function () {
+ hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit));
+ });
+
+ it('should return an array of values for _source fields', function () {
+ const extensions = getFieldValues({
+ hits,
+ field: indexPattern.fields.getByName('extension') as IndexPatternField,
+ indexPattern,
+ });
+ expect(extensions).toBeInstanceOf(Array);
+ expect(
+ _.filter(extensions, function (v) {
+ return v === 'html';
+ }).length
+ ).toBe(8);
+ expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']);
+ });
+
+ it('should return an array of values for core meta fields', function () {
+ const types = getFieldValues({
+ hits,
+ field: indexPattern.fields.getByName('_type') as IndexPatternField,
+ indexPattern,
+ });
+ expect(types).toBeInstanceOf(Array);
+ expect(
+ _.filter(types, function (v) {
+ return v === 'apache';
+ }).length
+ ).toBe(18);
+ expect(_.uniq(_.clone(types)).sort()).toEqual(['apache', 'nginx']);
+ });
+ });
+
+ describe('getFieldValueCounts', function () {
+ let params: FieldValueCountsParams;
+ beforeEach(function () {
+ params = {
+ hits: _.cloneDeep(realHits),
+ field: indexPattern.fields.getByName('extension') as IndexPatternField,
+ count: 3,
+ indexPattern,
+ };
+ });
+
+ it('counts the top 5 values by default', function () {
+ params.hits = params.hits.map((hit: Record, i) => ({
+ ...hit,
+ _source: {
+ extension: `${hit._source.extension}-${i}`,
+ },
+ }));
+ params.count = undefined;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(5);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('counts only distinct values if less than default', function () {
+ params.count = undefined;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(4);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('counts only distinct values if less than specified count', function () {
+ params.count = 10;
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(4);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('counts the top 3 values', function () {
+ const extensions = getFieldValueCounts(params);
+ expect(extensions).toBeInstanceOf(Object);
+ expect(extensions.buckets).toBeInstanceOf(Array);
+ const buckets = extensions.buckets as Bucket[];
+ expect(buckets.length).toBe(3);
+ expect(_.map(buckets, 'value')).toEqual(['html', 'gif', 'php']);
+ expect(extensions.error).toBeUndefined();
+ });
+
+ it('fails to analyze geo and attachment types', function () {
+ params.field = indexPattern.fields.getByName('point') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
+
+ params.field = indexPattern.fields.getByName('area') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
+
+ params.field = indexPattern.fields.getByName('request_body') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
+ });
+
+ it('fails to analyze fields that are in the mapping, but not the hits', function () {
+ params.field = indexPattern.fields.getByName('ip') as IndexPatternField;
+ expect(getFieldValueCounts(params).error).not.toBeUndefined();
+ });
+
+ it('counts the total hits', function () {
+ expect(getFieldValueCounts(params).total).toBe(params.hits.length);
+ });
+
+ it('counts the hits the field exists in', function () {
+ params.field = indexPattern.fields.getByName('phpmemory') as IndexPatternField;
+ expect(getFieldValueCounts(params).exists).toBe(5);
+ });
+
+ it('catches and returns errors', function () {
+ params.hits = params.hits.map((hit: Record) => ({
+ ...hit,
+ _source: {
+ extension: { foo: hit._source.extension },
+ },
+ }));
+ params.grouped = true;
+ expect(typeof getFieldValueCounts(params).error).toBe('string');
+ });
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts
new file mode 100644
index 000000000000..54f8832fa1fc
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_calculator.ts
@@ -0,0 +1,148 @@
+/*
+ * 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 { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
+import { FieldValueCounts } from '../types';
+
+const NO_ANALYSIS_TYPES = ['geo_point', 'geo_shape', 'attachment'];
+
+interface FieldValuesParams {
+ hits: Array>;
+ field: IndexPatternField;
+ indexPattern: IndexPattern;
+}
+
+interface FieldValueCountsParams extends FieldValuesParams {
+ count?: number;
+ grouped?: boolean;
+}
+
+const getFieldValues = ({ hits, field, indexPattern }: FieldValuesParams) => {
+ const name = field.name;
+ const flattenHit = indexPattern.flattenHit;
+ return hits.map((hit) => flattenHit(hit)[name]);
+};
+
+const getFieldValueCounts = (params: FieldValueCountsParams): FieldValueCounts => {
+ const { hits, field, indexPattern, count = 5, grouped = false } = params;
+ const { type: fieldType } = field;
+
+ if (NO_ANALYSIS_TYPES.includes(fieldType)) {
+ return {
+ error: i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage',
+ {
+ defaultMessage: 'Analysis is not available for {fieldType} fields.',
+ values: {
+ fieldType,
+ },
+ }
+ ),
+ };
+ }
+
+ const allValues = getFieldValues({ hits, field, indexPattern });
+ const missing = allValues.filter((v) => v === undefined || v === null).length;
+
+ try {
+ const groups = groupValues(allValues, grouped);
+ const counts = Object.keys(groups)
+ .sort((a, b) => groups[b].count - groups[a].count)
+ .slice(0, count)
+ .map((key) => ({
+ value: groups[key].value,
+ count: groups[key].count,
+ percent: (groups[key].count / (hits.length - missing)) * 100,
+ display: indexPattern.getFormatterForField(field).convert(groups[key].value),
+ }));
+
+ if (hits.length === missing) {
+ return {
+ error: i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage',
+ {
+ defaultMessage:
+ 'This field is present in your OpenSearch mapping but not in the {hitsLength} documents shown in the doc table. You may still be able to visualize or search on it.',
+ values: {
+ hitsLength: hits.length,
+ },
+ }
+ ),
+ };
+ }
+
+ return {
+ total: hits.length,
+ exists: hits.length - missing,
+ missing,
+ buckets: counts,
+ };
+ } catch (e) {
+ return {
+ error: e instanceof Error ? e.message : String(e),
+ };
+ }
+};
+
+const groupValues = (
+ allValues: any[],
+ grouped?: boolean
+): Record => {
+ const values = grouped ? allValues : allValues.flat();
+
+ return values
+ .filter((v) => {
+ if (v instanceof Object && !Array.isArray(v)) {
+ throw new Error(
+ i18n.translate(
+ 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage',
+ {
+ defaultMessage: 'Analysis is not available for object fields.',
+ }
+ )
+ );
+ }
+ return v !== undefined && v !== null;
+ })
+ .reduce((groups, value) => {
+ if (groups.hasOwnProperty(value)) {
+ groups[value].count++;
+ } else {
+ groups[value] = {
+ value,
+ count: 1,
+ };
+ }
+ return groups;
+ }, {});
+};
+
+export { FieldValueCountsParams, groupValues, getFieldValues, getFieldValueCounts };
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts
new file mode 100644
index 000000000000..a21d93cb5bc4
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.test.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 { getDefaultFieldFilter, setFieldFilterProp, isFieldFiltered } from './field_filter';
+import { IndexPatternField } from '../../../../../../data/public';
+
+describe('field_filter', function () {
+ it('getDefaultFieldFilter should return default filter state', function () {
+ expect(getDefaultFieldFilter()).toMatchInlineSnapshot(`
+ Object {
+ "aggregatable": null,
+ "missing": true,
+ "name": "",
+ "searchable": null,
+ "type": "any",
+ }
+ `);
+ });
+ it('setFieldFilterProp should return allow filter changes', function () {
+ const state = getDefaultFieldFilter();
+ const targetState = {
+ aggregatable: true,
+ missing: true,
+ name: 'test',
+ searchable: true,
+ type: 'string',
+ };
+ const actualState = Object.entries(targetState).reduce((acc, kv) => {
+ return setFieldFilterProp(acc, kv[0], kv[1]);
+ }, state);
+ expect(actualState).toMatchInlineSnapshot(`
+ Object {
+ "aggregatable": true,
+ "missing": true,
+ "name": "test",
+ "searchable": true,
+ "type": "string",
+ }
+ `);
+ });
+ it('filters a given list', () => {
+ const defaultState = getDefaultFieldFilter();
+ const fieldList = [
+ {
+ name: 'bytes',
+ type: 'number',
+ esTypes: ['long'],
+ count: 10,
+ scripted: false,
+ searchable: false,
+ aggregatable: false,
+ },
+ {
+ name: 'extension',
+ type: 'string',
+ esTypes: ['text'],
+ count: 10,
+ scripted: true,
+ searchable: true,
+ aggregatable: true,
+ },
+ ] as IndexPatternField[];
+
+ [
+ { filter: {}, result: ['bytes', 'extension'] },
+ { filter: { name: 'by' }, result: ['bytes'] },
+ { filter: { aggregatable: true }, result: ['extension'] },
+ { filter: { aggregatable: true, searchable: false }, result: [] },
+ { filter: { type: 'string' }, result: ['extension'] },
+ ].forEach((test) => {
+ const filtered = fieldList
+ .filter((field) =>
+ isFieldFiltered(field, { ...defaultState, ...test.filter }, { bytes: 1, extension: 1 })
+ )
+ .map((field) => field.name);
+
+ expect(filtered).toEqual(test.result);
+ });
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts
new file mode 100644
index 000000000000..d72af29b43e0
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/field_filter.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { IndexPatternField } from '../../../../../../data/public';
+
+export interface FieldFilterState {
+ missing: boolean;
+ type: string;
+ name: string;
+ aggregatable: null | boolean;
+ searchable: null | boolean;
+}
+
+export function getDefaultFieldFilter(): FieldFilterState {
+ return {
+ missing: true,
+ type: 'any',
+ name: '',
+ aggregatable: null,
+ searchable: null,
+ };
+}
+
+export function setFieldFilterProp(
+ state: FieldFilterState,
+ name: string,
+ value: string | boolean | null | undefined
+): FieldFilterState {
+ const newState = { ...state };
+ if (name === 'missing') {
+ newState.missing = Boolean(value);
+ } else if (name === 'aggregatable') {
+ newState.aggregatable = typeof value !== 'boolean' ? null : value;
+ } else if (name === 'searchable') {
+ newState.searchable = typeof value !== 'boolean' ? null : value;
+ } else if (name === 'name') {
+ newState.name = String(value);
+ } else if (name === 'type') {
+ newState.type = String(value);
+ }
+ return newState;
+}
+
+export function isFieldFiltered(
+ field: IndexPatternField,
+ filterState: FieldFilterState,
+ fieldCounts: Record
+): boolean {
+ const matchFilter = filterState.type === 'any' || field.type === filterState.type;
+ const isAggregatable =
+ filterState.aggregatable === null || field.aggregatable === filterState.aggregatable;
+ const isSearchable =
+ filterState.searchable === null || field.searchable === filterState.searchable;
+ const scriptedOrMissing =
+ !filterState.missing ||
+ field.type === '_source' ||
+ field.scripted ||
+ fieldCounts[field.name] > 0;
+ const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1;
+
+ return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts
new file mode 100644
index 000000000000..823cbde9ba72
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_details.ts
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+// @ts-ignore
+import { i18n } from '@osd/i18n';
+import { getFieldValueCounts } from './field_calculator';
+import { IndexPattern, IndexPatternField } from '../../../../../../data/public';
+
+export function getDetails(
+ field: IndexPatternField,
+ hits: Array>,
+ indexPattern?: IndexPattern
+) {
+ const defaultDetails = {
+ error: '',
+ exists: 0,
+ total: 0,
+ buckets: [],
+ };
+ if (!indexPattern) {
+ return {
+ ...defaultDetails,
+ error: i18n.translate('discover.fieldChooser.noIndexPatternSelectedErrorMessage', {
+ defaultMessage: 'Index pattern not specified.',
+ }),
+ };
+ }
+ const details = {
+ ...defaultDetails,
+ ...getFieldValueCounts({
+ hits,
+ field,
+ indexPattern,
+ count: 5,
+ grouped: false,
+ }),
+ };
+ if (details.buckets) {
+ for (const bucket of details.buckets) {
+ bucket.display = indexPattern.getFormatterForField(field).convert(bucket.value);
+ }
+ }
+ return details;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts
new file mode 100644
index 000000000000..38b18792d3e4
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_field_type_name.ts
@@ -0,0 +1,85 @@
+/*
+ * 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';
+
+export function getFieldTypeName(type: string) {
+ switch (type) {
+ case 'boolean':
+ return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', {
+ defaultMessage: 'Boolean field',
+ });
+ case 'conflict':
+ return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', {
+ defaultMessage: 'Conflicting field',
+ });
+ case 'date':
+ return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', {
+ defaultMessage: 'Date field',
+ });
+ case 'geo_point':
+ return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', {
+ defaultMessage: 'Geo point field',
+ });
+ case 'geo_shape':
+ return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', {
+ defaultMessage: 'Geo shape field',
+ });
+ case 'ip':
+ return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', {
+ defaultMessage: 'IP address field',
+ });
+ case 'murmur3':
+ return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', {
+ defaultMessage: 'Murmur3 field',
+ });
+ case 'number':
+ return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', {
+ defaultMessage: 'Number field',
+ });
+ case 'source':
+ // Note that this type is currently not provided, type for _source is undefined
+ return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', {
+ defaultMessage: 'Source field',
+ });
+ case 'string':
+ return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', {
+ defaultMessage: 'String field',
+ });
+ case 'nested':
+ return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', {
+ defaultMessage: 'Nested field',
+ });
+ default:
+ return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', {
+ defaultMessage: 'Unknown field',
+ });
+ }
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts
new file mode 100644
index 000000000000..b3a8ff5cd8d9
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_index_pattern_field_list.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { difference } from 'lodash';
+import { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
+
+export function getIndexPatternFieldList(
+ indexPattern?: IndexPattern,
+ fieldCounts?: Record
+) {
+ if (!indexPattern || !fieldCounts) return [];
+
+ const fieldNamesInDocs = Object.keys(fieldCounts);
+ const fieldNamesInIndexPattern = indexPattern.fields.getAll().map((fld) => fld.name);
+ const unknownTypes: IndexPatternField[] = [];
+
+ difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach((unknownFieldName) => {
+ unknownTypes.push({
+ displayName: String(unknownFieldName),
+ name: String(unknownFieldName),
+ type: 'unknown',
+ } as IndexPatternField);
+ });
+
+ return [...indexPattern.fields.getAll(), ...unknownTypes];
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts
new file mode 100644
index 000000000000..770a0ce664e4
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/get_warnings.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { IndexPatternField } from '../../../../../../data/public';
+
+export function getWarnings(field: IndexPatternField) {
+ let warnings = [];
+
+ if (field.scripted) {
+ warnings.push(
+ i18n.translate(
+ 'discover.fieldChooser.discoverField.scriptedFieldsTakeLongExecuteDescription',
+ {
+ defaultMessage: 'Scripted fields can take a long time to execute.',
+ }
+ )
+ );
+ }
+
+ if (warnings.length > 1) {
+ warnings = warnings.map(function (warning, i) {
+ return (i > 0 ? '\n' : '') + (i + 1) + ' - ' + warning;
+ });
+ }
+
+ return warnings;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts
new file mode 100644
index 000000000000..7301ce3a4c96
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.test.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 { groupFields } from './group_fields';
+import { getDefaultFieldFilter } from './field_filter';
+
+describe('group_fields', function () {
+ it('should group fields in selected, popular, unpopular group', function () {
+ const fields = [
+ {
+ name: 'category',
+ type: 'string',
+ esTypes: ['text'],
+ count: 1,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ name: 'currency',
+ type: 'string',
+ esTypes: ['keyword'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ name: 'customer_birth_date',
+ type: 'date',
+ esTypes: ['date'],
+ count: 0,
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ ];
+
+ const fieldCounts = {
+ category: 1,
+ currency: 1,
+ customer_birth_date: 1,
+ };
+
+ const fieldFilterState = getDefaultFieldFilter();
+
+ const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState);
+ expect(actual).toMatchInlineSnapshot(`
+ Object {
+ "popular": Array [
+ Object {
+ "aggregatable": true,
+ "count": 1,
+ "esTypes": Array [
+ "text",
+ ],
+ "name": "category",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "type": "string",
+ },
+ ],
+ "selected": Array [
+ Object {
+ "aggregatable": true,
+ "count": 0,
+ "esTypes": Array [
+ "keyword",
+ ],
+ "name": "currency",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "type": "string",
+ },
+ ],
+ "unpopular": Array [
+ Object {
+ "aggregatable": true,
+ "count": 0,
+ "esTypes": Array [
+ "date",
+ ],
+ "name": "customer_birth_date",
+ "readFromDocValues": true,
+ "scripted": false,
+ "searchable": true,
+ "type": "date",
+ },
+ ],
+ }
+ `);
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx
new file mode 100644
index 000000000000..fad1db402467
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/group_fields.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { IndexPatternField } from 'src/plugins/data/public';
+import { FieldFilterState, isFieldFiltered } from './field_filter';
+
+interface GroupedFields {
+ selected: IndexPatternField[];
+ popular: IndexPatternField[];
+ unpopular: IndexPatternField[];
+}
+
+/**
+ * group the fields into selected, popular and unpopular, filter by fieldFilterState
+ */
+export function groupFields(
+ fields: IndexPatternField[] | null,
+ columns: string[],
+ popularLimit: number,
+ fieldCounts: Record,
+ fieldFilterState: FieldFilterState
+): GroupedFields {
+ const result: GroupedFields = {
+ selected: [],
+ popular: [],
+ unpopular: [],
+ };
+ if (!Array.isArray(fields) || !Array.isArray(columns) || typeof fieldCounts !== 'object') {
+ return result;
+ }
+
+ const popular = fields
+ .filter((field) => !columns.includes(field.name) && field.count)
+ .sort((a: IndexPatternField, b: IndexPatternField) => (b.count || 0) - (a.count || 0))
+ .map((field) => field.name)
+ .slice(0, popularLimit);
+
+ const compareFn = (a: IndexPatternField, b: IndexPatternField) => {
+ if (!a.displayName) {
+ return 0;
+ }
+ return a.displayName.localeCompare(b.displayName || '');
+ };
+ const fieldsSorted = fields.sort(compareFn);
+
+ for (const field of fieldsSorted) {
+ if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) {
+ continue;
+ }
+ if (columns.includes(field.name)) {
+ result.selected.push(field);
+ } else if (popular.includes(field.name) && field.type !== '_source') {
+ result.popular.push(field);
+ } else if (field.type !== '_source') {
+ result.unpopular.push(field);
+ }
+ }
+
+ return result;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts b/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts
new file mode 100644
index 000000000000..36a6bcf2e329
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/lib/visualize_trigger_utils.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 {
+ VISUALIZE_FIELD_TRIGGER,
+ VISUALIZE_GEO_FIELD_TRIGGER,
+ visualizeFieldTrigger,
+ visualizeGeoFieldTrigger,
+} from '../../../../../../ui_actions/public';
+import { getUiActions } from '../../../../opensearch_dashboards_services';
+import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../../data/public';
+
+function getTriggerConstant(type: string) {
+ return type === OSD_FIELD_TYPES.GEO_POINT || type === OSD_FIELD_TYPES.GEO_SHAPE
+ ? VISUALIZE_GEO_FIELD_TRIGGER
+ : VISUALIZE_FIELD_TRIGGER;
+}
+
+function getTrigger(type: string) {
+ return type === OSD_FIELD_TYPES.GEO_POINT || type === OSD_FIELD_TYPES.GEO_SHAPE
+ ? visualizeGeoFieldTrigger
+ : visualizeFieldTrigger;
+}
+
+async function getCompatibleActions(
+ fieldName: string,
+ indexPatternId: string,
+ contextualFields: string[],
+ trigger: typeof VISUALIZE_FIELD_TRIGGER | typeof VISUALIZE_GEO_FIELD_TRIGGER
+) {
+ const compatibleActions = await getUiActions().getTriggerCompatibleActions(trigger, {
+ indexPatternId,
+ fieldName,
+ contextualFields,
+ });
+ return compatibleActions;
+}
+
+export async function getVisualizeHref(
+ field: IndexPatternField,
+ indexPatternId: string | undefined,
+ contextualFields: string[]
+) {
+ if (!indexPatternId) return undefined;
+ const triggerOptions = {
+ indexPatternId,
+ fieldName: field.name,
+ contextualFields,
+ trigger: getTrigger(field.type),
+ };
+ const compatibleActions = await getCompatibleActions(
+ field.name,
+ indexPatternId,
+ contextualFields,
+ getTriggerConstant(field.type)
+ );
+ // enable the link only if only one action is registered
+ return compatibleActions.length === 1
+ ? compatibleActions[0].getHref?.(triggerOptions)
+ : undefined;
+}
+
+export function triggerVisualizeActions(
+ field: IndexPatternField,
+ indexPatternId: string | undefined,
+ contextualFields: string[]
+) {
+ if (!indexPatternId) return;
+ const trigger = getTriggerConstant(field.type);
+ const triggerOptions = {
+ indexPatternId,
+ fieldName: field.name,
+ contextualFields,
+ };
+ getUiActions().getTrigger(trigger).exec(triggerOptions);
+}
+
+export async function isFieldVisualizable(
+ field: IndexPatternField,
+ indexPatternId: string | undefined,
+ contextualFields: string[]
+) {
+ if (field.name === '_id' || !indexPatternId) {
+ // for first condition you'd get a 'Fielddata access on the _id field is disallowed' error on OpenSearch side.
+ return false;
+ }
+ const trigger = getTriggerConstant(field.type);
+ const compatibleActions = await getCompatibleActions(
+ field.name,
+ indexPatternId,
+ contextualFields,
+ trigger
+ );
+ return compatibleActions.length > 0 && field.visualizable;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx b/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx
new file mode 100644
index 000000000000..dba087d0f9ed
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/string_progress_bar.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 from 'react';
+import { EuiProgress } from '@elastic/eui';
+
+interface Props {
+ percent: number;
+ count: number;
+ value: string;
+}
+
+export function StringFieldProgressBar({ value, percent, count }: Props) {
+ const ariaLabel = `${value}: ${count} (${percent}%)`;
+
+ return (
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/sidebar/types.ts b/src/plugins/discover_legacy/public/application/components/sidebar/types.ts
new file mode 100644
index 000000000000..a43120b28e96
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/sidebar/types.ts
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+export interface IndexPatternRef {
+ id: string;
+ title: string;
+}
+
+export interface FieldDetails {
+ error: string;
+ exists: number;
+ total: number;
+ buckets: Bucket[];
+}
+
+export interface FieldValueCounts extends Partial {
+ missing?: number;
+}
+
+export interface Bucket {
+ display: string;
+ value: string;
+ percent: number;
+ count: number;
+}
diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts
new file mode 100644
index 000000000000..094d8e286875
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/index.ts
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+export { SkipBottomButton } from './skip_bottom_button';
diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx
new file mode 100644
index 000000000000..28ffef9dae86
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ReactWrapper } from 'enzyme';
+import { SkipBottomButton, SkipBottomButtonProps } from './skip_bottom_button';
+
+describe('Skip to Bottom Button', function () {
+ let props: SkipBottomButtonProps;
+ let component: ReactWrapper;
+
+ beforeAll(() => {
+ props = {
+ onClick: jest.fn(),
+ };
+ });
+
+ it('should be clickable', function () {
+ component = mountWithIntl();
+ component.simulate('click');
+ expect(props.onClick).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx
new file mode 100644
index 000000000000..a1e5754cb312
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/skip_bottom_button/skip_bottom_button.tsx
@@ -0,0 +1,66 @@
+/*
+ * 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 from 'react';
+import { EuiSkipLink } from '@elastic/eui';
+import { FormattedMessage, I18nProvider } from '@osd/i18n/react';
+
+export interface SkipBottomButtonProps {
+ /**
+ * Action to perform on click
+ */
+ onClick: () => void;
+}
+
+export function SkipBottomButton({ onClick }: SkipBottomButtonProps) {
+ return (
+
+ {
+ // prevent the anchor to reload the page on click
+ event.preventDefault();
+ // The destinationId prop cannot be leveraged here as the table needs
+ // to be updated first (angular logic)
+ onClick();
+ }}
+ className="dscSkipButton"
+ destinationId=""
+ data-test-subj="discoverSkipTableButton"
+ >
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table.test.tsx b/src/plugins/discover_legacy/public/application/components/table/table.test.tsx
new file mode 100644
index 000000000000..220ac57feae2
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table.test.tsx
@@ -0,0 +1,279 @@
+/*
+ * 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 from 'react';
+import { mount } from 'enzyme';
+import { findTestSubject } from 'test_utils/helpers';
+import { DocViewTable } from './table';
+import { indexPatterns, IndexPattern } from '../../../../../data/public';
+
+const indexPattern = ({
+ fields: {
+ getAll: () => [
+ {
+ name: '_index',
+ type: 'string',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'message',
+ type: 'string',
+ scripted: false,
+ filterable: false,
+ },
+ {
+ name: 'extension',
+ type: 'string',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'bytes',
+ type: 'number',
+ scripted: false,
+ filterable: true,
+ },
+ {
+ name: 'scripted',
+ type: 'number',
+ scripted: true,
+ filterable: false,
+ },
+ ],
+ },
+ metaFields: ['_index', '_score'],
+ flattenHit: undefined,
+ formatHit: jest.fn((hit) => hit._source),
+} as unknown) as IndexPattern;
+
+indexPattern.fields.getByName = (name: string) => {
+ return indexPattern.fields.getAll().find((field) => field.name === name);
+};
+
+indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
+
+describe('DocViewTable at Discover', () => {
+ // At Discover's main view, all buttons are rendered
+ // check for existence of action buttons and warnings
+
+ const hit = {
+ _index: 'logstash-2014.09.09',
+ _type: 'doc',
+ _id: 'id123',
+ _score: 1,
+ _source: {
+ message:
+ 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
+ Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \
+ et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \
+ ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
+ Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \
+ rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \
+ Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \
+ Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut',
+ extension: 'html',
+ not_mapped: 'yes',
+ bytes: 100,
+ objectArray: [{ foo: true }],
+ relatedContent: {
+ test: 1,
+ },
+ scripted: 123,
+ _underscore: 123,
+ },
+ };
+
+ const props = {
+ hit,
+ columns: ['extension'],
+ indexPattern,
+ filter: jest.fn(),
+ onAddColumn: jest.fn(),
+ onRemoveColumn: jest.fn(),
+ };
+ const component = mount();
+ [
+ {
+ _property: '_index',
+ addInclusiveFilterButton: true,
+ collapseBtn: false,
+ noMappingWarning: false,
+ toggleColumnButton: true,
+ underscoreWarning: false,
+ },
+ {
+ _property: 'message',
+ addInclusiveFilterButton: false,
+ collapseBtn: true,
+ noMappingWarning: false,
+ toggleColumnButton: true,
+ underscoreWarning: false,
+ },
+ {
+ _property: '_underscore',
+ addInclusiveFilterButton: false,
+ collapseBtn: false,
+ noMappingWarning: false,
+ toggleColumnButton: true,
+ underScoreWarning: true,
+ },
+ {
+ _property: 'scripted',
+ addInclusiveFilterButton: false,
+ collapseBtn: false,
+ noMappingWarning: false,
+ toggleColumnButton: true,
+ underScoreWarning: false,
+ },
+ {
+ _property: 'not_mapped',
+ addInclusiveFilterButton: false,
+ collapseBtn: false,
+ noMappingWarning: true,
+ toggleColumnButton: true,
+ underScoreWarning: false,
+ },
+ ].forEach((check) => {
+ const rowComponent = findTestSubject(component, `tableDocViewRow-${check._property}`);
+
+ it(`renders row for ${check._property}`, () => {
+ expect(rowComponent.length).toBe(1);
+ });
+
+ ([
+ 'addInclusiveFilterButton',
+ 'collapseBtn',
+ 'toggleColumnButton',
+ 'underscoreWarning',
+ ] as const).forEach((element) => {
+ const elementExist = check[element];
+
+ if (typeof elementExist === 'boolean') {
+ const btn = findTestSubject(rowComponent, element);
+
+ it(`renders ${element} for '${check._property}' correctly`, () => {
+ const disabled = btn.length ? btn.props().disabled : true;
+ const clickAble = btn.length && !disabled ? true : false;
+ expect(clickAble).toBe(elementExist);
+ });
+ }
+ });
+
+ (['noMappingWarning'] as const).forEach((element) => {
+ const elementExist = check[element];
+
+ if (typeof elementExist === 'boolean') {
+ const el = findTestSubject(rowComponent, element);
+
+ it(`renders ${element} for '${check._property}' correctly`, () => {
+ expect(el.length).toBe(elementExist ? 1 : 0);
+ });
+ }
+ });
+ });
+});
+
+describe('DocViewTable at Discover Doc', () => {
+ const hit = {
+ _index: 'logstash-2014.09.09',
+ _score: 1,
+ _type: 'doc',
+ _id: 'id123',
+ _source: {
+ extension: 'html',
+ not_mapped: 'yes',
+ },
+ };
+ // here no action buttons are rendered
+ const props = {
+ hit,
+ indexPattern,
+ };
+ const component = mount();
+ const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length;
+
+ it(`renders no action buttons`, () => {
+ expect(foundLength).toBe(0);
+ });
+});
+
+describe('DocViewTable at Discover Context', () => {
+ // here no toggleColumnButtons are rendered
+ const hit = {
+ _index: 'logstash-2014.09.09',
+ _type: 'doc',
+ _id: 'id123',
+ _score: 1,
+ _source: {
+ message:
+ 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
+ Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \
+ et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \
+ ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
+ Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \
+ rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \
+ Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \
+ Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut',
+ },
+ };
+ const props = {
+ hit,
+ columns: ['extension'],
+ indexPattern,
+ filter: jest.fn(),
+ };
+
+ const component = mount();
+
+ it(`renders no toggleColumnButton`, () => {
+ const foundLength = findTestSubject(component, 'toggleColumnButtons').length;
+ expect(foundLength).toBe(0);
+ });
+
+ it(`renders addInclusiveFilterButton`, () => {
+ const row = findTestSubject(component, `tableDocViewRow-_index`);
+ const btn = findTestSubject(row, 'addInclusiveFilterButton');
+ expect(btn.length).toBe(1);
+ btn.simulate('click');
+ expect(props.filter).toBeCalled();
+ });
+
+ it(`renders functional collapse button`, () => {
+ const btn = findTestSubject(component, `collapseBtn`);
+ const html = component.html();
+
+ expect(component.html()).toContain('truncate-by-height');
+
+ expect(btn.length).toBe(1);
+ btn.simulate('click');
+ expect(component.html() !== html).toBeTruthy();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/table/table.tsx b/src/plugins/discover_legacy/public/application/components/table/table.tsx
new file mode 100644
index 000000000000..90167a515985
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table.tsx
@@ -0,0 +1,149 @@
+/*
+ * 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 } from 'react';
+import { escapeRegExp } from 'lodash';
+import { DocViewTableRow } from './table_row';
+import { arrayContainsObjects, trimAngularSpan } from './table_helper';
+import { DocViewRenderProps } from '../../doc_views/doc_views_types';
+
+const COLLAPSE_LINE_LENGTH = 350;
+
+export function DocViewTable({
+ hit,
+ indexPattern,
+ filter,
+ columns,
+ onAddColumn,
+ onRemoveColumn,
+}: DocViewRenderProps) {
+ const mapping = indexPattern.fields.getByName;
+ const flattened = indexPattern.flattenHit(hit);
+ const formatted = indexPattern.formatHit(hit, 'html');
+ const [fieldRowOpen, setFieldRowOpen] = useState({} as Record);
+
+ function toggleValueCollapse(field: string) {
+ fieldRowOpen[field] = fieldRowOpen[field] !== true;
+ setFieldRowOpen({ ...fieldRowOpen });
+ }
+
+ return (
+
+
+ {Object.keys(flattened)
+ .sort()
+ .map((field) => {
+ const valueRaw = flattened[field];
+ const value = trimAngularSpan(String(formatted[field]));
+
+ const isCollapsible = value.length > COLLAPSE_LINE_LENGTH;
+ const isCollapsed = isCollapsible && !fieldRowOpen[field];
+ const toggleColumn =
+ onRemoveColumn && onAddColumn && Array.isArray(columns)
+ ? () => {
+ if (columns.includes(field)) {
+ onRemoveColumn(field);
+ } else {
+ onAddColumn(field);
+ }
+ }
+ : undefined;
+ const isArrayOfObjects =
+ Array.isArray(flattened[field]) && arrayContainsObjects(flattened[field]);
+ const displayUnderscoreWarning = !mapping(field) && field.indexOf('_') === 0;
+ const displayNoMappingWarning =
+ !mapping(field) && !displayUnderscoreWarning && !isArrayOfObjects;
+
+ // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that
+ // contains an array, Discover will only detect the top level root field. We want to detect when those
+ // root fields are `nested` so that we can display the proper icon and label. However, those root
+ // `nested` fields are not a part of the index pattern. Their children are though, and contain nested path
+ // info. So to detect nested fields we look through the index pattern for nested children
+ // whose path begins with the current field. There are edge cases where
+ // this could incorrectly identify a plain `object` field as `nested`. Say we had the following document
+ // where `foo` is a plain object field and `bar` is a nested field.
+ // {
+ // "foo": [
+ // {
+ // "bar": [
+ // {
+ // "baz": "qux"
+ // }
+ // ]
+ // },
+ // {
+ // "bar": [
+ // {
+ // "baz": "qux"
+ // }
+ // ]
+ // }
+ // ]
+ // }
+ //
+ // The following code will search for `foo`, find it at the beginning of the path to the nested child field
+ // `foo.bar.baz` and incorrectly mark `foo` as nested. Any time we're searching for the name of a plain object
+ // field that happens to match a segment of a nested path, we'll get a false positive.
+ // We're aware of this issue and we'll have to live with
+ // it in the short term. The long term fix will be to add info about the `nested` and `object` root fields
+ // to the index pattern, but that has its own complications which you can read more about in the following
+ // issue: https://github.com/elastic/kibana/issues/54957
+ const isNestedField =
+ !indexPattern.fields.getByName(field) &&
+ !!indexPattern.fields.getAll().find((patternField) => {
+ // We only want to match a full path segment
+ const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)');
+ return nestedRootRegex.test(patternField.subType?.nested?.path ?? '');
+ });
+ const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type;
+
+ return (
+ toggleValueCollapse(field)}
+ onToggleColumn={toggleColumn}
+ value={value}
+ valueRaw={valueRaw}
+ />
+ );
+ })}
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts b/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts
new file mode 100644
index 000000000000..20c1092ef86d
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_helper.test.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { arrayContainsObjects } from './table_helper';
+
+describe('arrayContainsObjects', () => {
+ it(`returns false for an array of primitives`, () => {
+ const actual = arrayContainsObjects(['test', 'test']);
+ expect(actual).toBeFalsy();
+ });
+
+ it(`returns true for an array of objects`, () => {
+ const actual = arrayContainsObjects([{}, {}]);
+ expect(actual).toBeTruthy();
+ });
+
+ it(`returns true for an array of objects and primitves`, () => {
+ const actual = arrayContainsObjects([{}, 'sdf']);
+ expect(actual).toBeTruthy();
+ });
+
+ it(`returns false for an array of null values`, () => {
+ const actual = arrayContainsObjects([null, null]);
+ expect(actual).toBeFalsy();
+ });
+
+ it(`returns false if no array is given`, () => {
+ const actual = arrayContainsObjects([null, null]);
+ expect(actual).toBeFalsy();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx b/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx
new file mode 100644
index 000000000000..2e63b43b8310
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_helper.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+/**
+ * Returns true if the given array contains at least 1 object
+ */
+export function arrayContainsObjects(value: unknown[]): boolean {
+ return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null);
+}
+
+/**
+ * Removes markup added by OpenSearch Dashboards fields html formatter
+ */
+export function trimAngularSpan(text: string): string {
+ return text.replace(/^/, '').replace(/<\/span>$/, '');
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row.tsx
new file mode 100644
index 000000000000..95ba38106e3e
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row.tsx
@@ -0,0 +1,129 @@
+/*
+ * 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 classNames from 'classnames';
+import React, { ReactNode } from 'react';
+import { FieldMapping, DocViewFilterFn } from '../../doc_views/doc_views_types';
+import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add';
+import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove';
+import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column';
+import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse';
+import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists';
+import { DocViewTableRowIconNoMapping } from './table_row_icon_no_mapping';
+import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore';
+import { FieldName } from '../field_name/field_name';
+
+export interface Props {
+ field: string;
+ fieldMapping?: FieldMapping;
+ fieldType: string;
+ displayNoMappingWarning: boolean;
+ displayUnderscoreWarning: boolean;
+ isCollapsible: boolean;
+ isColumnActive: boolean;
+ isCollapsed: boolean;
+ onToggleCollapse: () => void;
+ onFilter?: DocViewFilterFn;
+ onToggleColumn?: () => void;
+ value: string | ReactNode;
+ valueRaw: unknown;
+}
+
+export function DocViewTableRow({
+ field,
+ fieldMapping,
+ fieldType,
+ displayNoMappingWarning,
+ displayUnderscoreWarning,
+ isCollapsible,
+ isCollapsed,
+ isColumnActive,
+ onFilter,
+ onToggleCollapse,
+ onToggleColumn,
+ value,
+ valueRaw,
+}: Props) {
+ const valueClassName = classNames({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ osdDocViewer__value: true,
+ 'truncate-by-height': isCollapsible && isCollapsed,
+ });
+
+ return (
+
+ {typeof onFilter === 'function' && (
+
+ onFilter(fieldMapping, valueRaw, '+')}
+ />
+ onFilter(fieldMapping, valueRaw, '-')}
+ />
+ {typeof onToggleColumn === 'function' && (
+
+ )}
+ onFilter('_exists_', field, '+')}
+ scripted={fieldMapping && fieldMapping.scripted}
+ />
+ |
+ )}
+
+
+ |
+
+ {isCollapsible && (
+
+ )}
+ {displayUnderscoreWarning && }
+ {displayNoMappingWarning && }
+
+ |
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx
new file mode 100644
index 000000000000..de25c73e9c95
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_collapse.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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 from 'react';
+import { i18n } from '@osd/i18n';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+
+export interface Props {
+ onClick: () => void;
+ isCollapsed: boolean;
+}
+
+export function DocViewTableRowBtnCollapse({ onClick, isCollapsed }: Props) {
+ const label = i18n.translate('discover.docViews.table.toggleFieldDetails', {
+ defaultMessage: 'Toggle field details',
+ });
+ return (
+
+ onClick()}
+ iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
+ iconSize={'s'}
+ />
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx
new file mode 100644
index 000000000000..1707861faf28
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_add.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export interface Props {
+ onClick: () => void;
+ disabled: boolean;
+}
+
+export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props) {
+ const tooltipContent = disabled ? (
+
+ ) : (
+
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx
new file mode 100644
index 000000000000..d4f401282e14
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_exists.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export interface Props {
+ onClick: () => void;
+ disabled?: boolean;
+ scripted?: boolean;
+}
+
+export function DocViewTableRowBtnFilterExists({
+ onClick,
+ disabled = false,
+ scripted = false,
+}: Props) {
+ const tooltipContent = disabled ? (
+ scripted ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx
new file mode 100644
index 000000000000..3b58fbfdc282
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_filter_remove.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export interface Props {
+ onClick: () => void;
+ disabled?: boolean;
+}
+
+export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Props) {
+ const tooltipContent = disabled ? (
+
+ ) : (
+
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx
new file mode 100644
index 000000000000..74f0972fa0ee
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_btn_toggle_column.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export interface Props {
+ active: boolean;
+ disabled?: boolean;
+ onClick: () => void;
+}
+
+export function DocViewTableRowBtnToggleColumn({ onClick, active, disabled = false }: Props) {
+ if (disabled) {
+ return (
+
+ );
+ }
+ return (
+
+ }
+ >
+
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx
new file mode 100644
index 000000000000..edc4bea91bd8
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_no_mapping.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 from 'react';
+import { EuiIconTip } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export function DocViewTableRowIconNoMapping() {
+ const ariaLabel = i18n.translate('discover.docViews.table.noCachedMappingForThisFieldAriaLabel', {
+ defaultMessage: 'Warning',
+ });
+ const tooltipContent = i18n.translate(
+ 'discover.docViews.table.noCachedMappingForThisFieldTooltip',
+ {
+ defaultMessage:
+ 'No cached mapping for this field. Refresh field list from the Management > Index Patterns page',
+ }
+ );
+ return (
+
+ );
+}
diff --git a/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx
new file mode 100644
index 000000000000..f1d09e2c8d44
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/table/table_row_icon_underscore.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 from 'react';
+import { EuiIconTip } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+
+export function DocViewTableRowIconUnderscore() {
+ const ariaLabel = i18n.translate(
+ 'discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel',
+ {
+ defaultMessage: 'Warning',
+ }
+ );
+ const tooltipContent = i18n.translate(
+ 'discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedTooltip',
+ {
+ defaultMessage: 'Field names beginning with {underscoreSign} are not supported',
+ values: { underscoreSign: '_' },
+ }
+ );
+
+ return (
+
+ );
+}
diff --git a/src/plugins/discover/public/application/components/timechart_header/index.ts b/src/plugins/discover_legacy/public/application/components/timechart_header/index.ts
similarity index 100%
rename from src/plugins/discover/public/application/components/timechart_header/index.ts
rename to src/plugins/discover_legacy/public/application/components/timechart_header/index.ts
diff --git a/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx
new file mode 100644
index 000000000000..9011c38a6acb
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.test.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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 from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ReactWrapper } from 'enzyme';
+import { TimechartHeader, TimechartHeaderProps } from './timechart_header';
+import { EuiIconTip } from '@elastic/eui';
+import { findTestSubject } from 'test_utils/helpers';
+
+describe('timechart header', function () {
+ let props: TimechartHeaderProps;
+ let component: ReactWrapper;
+
+ beforeAll(() => {
+ props = {
+ timeRange: {
+ from: 'May 14, 2020 @ 11:05:13.590',
+ to: 'May 14, 2020 @ 11:20:13.590',
+ },
+ stateInterval: 's',
+ options: [
+ {
+ display: 'Auto',
+ val: 'auto',
+ },
+ {
+ display: 'Millisecond',
+ val: 'ms',
+ },
+ {
+ display: 'Second',
+ val: 's',
+ },
+ ],
+ onChangeInterval: jest.fn(),
+ bucketInterval: {
+ scaled: undefined,
+ description: 'second',
+ scale: undefined,
+ },
+ };
+ });
+
+ it('TimechartHeader not renders an info text when the showScaledInfo property is not provided', () => {
+ component = mountWithIntl();
+ expect(component.find(EuiIconTip).length).toBe(0);
+ });
+
+ it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => {
+ props.bucketInterval!.scaled = true;
+ component = mountWithIntl();
+ expect(component.find(EuiIconTip).length).toBe(1);
+ });
+
+ it('expect to render the date range', function () {
+ component = mountWithIntl();
+ const datetimeRangeText = findTestSubject(component, 'discoverIntervalDateRange');
+ expect(datetimeRangeText.text()).toBe(
+ 'May 14, 2020 @ 11:05:13.590 - May 14, 2020 @ 11:20:13.590 per'
+ );
+ });
+
+ it('expects to render a dropdown with the interval options', () => {
+ component = mountWithIntl();
+ const dropdown = findTestSubject(component, 'discoverIntervalSelect');
+ expect(dropdown.length).toBe(1);
+ // @ts-ignore
+ const values = dropdown.find('option').map((option) => option.prop('value'));
+ expect(values).toEqual(['auto', 'ms', 's']);
+ // @ts-ignore
+ const labels = dropdown.find('option').map((option) => option.text());
+ expect(labels).toEqual(['Auto', 'Millisecond', 'Second']);
+ });
+
+ it('should change the interval', function () {
+ component = mountWithIntl();
+ findTestSubject(component, 'discoverIntervalSelect').simulate('change', {
+ target: { value: 'ms' },
+ });
+ expect(props.onChangeInterval).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.tsx
similarity index 100%
rename from src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx
rename to src/plugins/discover_legacy/public/application/components/timechart_header/timechart_header.tsx
diff --git a/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap b/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap
new file mode 100644
index 000000000000..342dea206c30
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/components/top_nav/__snapshots__/open_search_panel.test.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render 1`] = `
+
+
+
+
+
+
+
+
+
+
+ }
+ onChoose={[Function]}
+ savedObjectMetaData={
+ Array [
+ Object {
+ "getIconForSavedObject": [Function],
+ "name": "Saved search",
+ "type": "search",
+ },
+ ]
+ }
+ savedObjects={Object {}}
+ uiSettings={Object {}}
+ />
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.js b/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js
similarity index 100%
rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.js
rename to src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.js
diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js b/src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.test.js
similarity index 100%
rename from src/plugins/discover/public/application/components/top_nav/open_search_panel.test.js
rename to src/plugins/discover_legacy/public/application/components/top_nav/open_search_panel.test.js
diff --git a/src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js b/src/plugins/discover_legacy/public/application/components/top_nav/show_open_search_panel.js
similarity index 100%
rename from src/plugins/discover/public/application/components/top_nav/show_open_search_panel.js
rename to src/plugins/discover_legacy/public/application/components/top_nav/show_open_search_panel.js
diff --git a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx b/src/plugins/discover_legacy/public/application/doc_views/doc_views_helpers.tsx
similarity index 100%
rename from src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx
rename to src/plugins/discover_legacy/public/application/doc_views/doc_views_helpers.tsx
diff --git a/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts
new file mode 100644
index 000000000000..56f167b5f2cc
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/doc_views/doc_views_registry.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { auto } from 'angular';
+import { convertDirectiveToRenderFn } from './doc_views_helpers';
+import { DocView, DocViewInput, OpenSearchSearchHit, DocViewInputFn } from './doc_views_types';
+
+export class DocViewsRegistry {
+ private docViews: DocView[] = [];
+ private angularInjectorGetter: (() => Promise) | null = null;
+
+ setAngularInjectorGetter = (injectorGetter: () => Promise) => {
+ this.angularInjectorGetter = injectorGetter;
+ };
+
+ /**
+ * Extends and adds the given doc view to the registry array
+ */
+ addDocView(docViewRaw: DocViewInput | DocViewInputFn) {
+ const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw;
+ if (docView.directive) {
+ // convert angular directive to render function for backwards compatibility
+ docView.render = convertDirectiveToRenderFn(docView.directive, () => {
+ if (!this.angularInjectorGetter) {
+ throw new Error('Angular was not initialized');
+ }
+ return this.angularInjectorGetter();
+ });
+ }
+ if (typeof docView.shouldShow !== 'function') {
+ docView.shouldShow = () => true;
+ }
+ this.docViews.push(docView as DocView);
+ }
+ /**
+ * Returns a sorted array of doc_views for rendering tabs
+ */
+ getDocViewsSorted(hit: OpenSearchSearchHit) {
+ return this.docViews
+ .filter((docView) => docView.shouldShow(hit))
+ .sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1));
+ }
+}
diff --git a/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts b/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts
new file mode 100644
index 000000000000..961fc98516f6
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/doc_views/doc_views_types.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { ComponentType } from 'react';
+import { IScope } from 'angular';
+import { SearchResponse } from 'elasticsearch';
+import { IndexPattern } from '../../../../data/public';
+
+export interface AngularDirective {
+ controller: (...injectedServices: any[]) => void;
+ template: string;
+}
+
+export type AngularScope = IScope;
+
+export type OpenSearchSearchHit = SearchResponse['hits']['hits'][number];
+
+export interface FieldMapping {
+ filterable?: boolean;
+ scripted?: boolean;
+ rowCount?: number;
+ type: string;
+ name: string;
+}
+
+export type DocViewFilterFn = (
+ mapping: FieldMapping | string | undefined,
+ value: unknown,
+ mode: '+' | '-'
+) => void;
+
+export interface DocViewRenderProps {
+ columns?: string[];
+ filter?: DocViewFilterFn;
+ hit: OpenSearchSearchHit;
+ indexPattern: IndexPattern;
+ onAddColumn?: (columnName: string) => void;
+ onRemoveColumn?: (columnName: string) => void;
+}
+export type DocViewerComponent = ComponentType;
+export type DocViewRenderFn = (
+ domeNode: HTMLDivElement,
+ renderProps: DocViewRenderProps
+) => () => void;
+
+export interface DocViewInput {
+ component?: DocViewerComponent;
+ directive?: AngularDirective;
+ order: number;
+ render?: DocViewRenderFn;
+ shouldShow?: (hit: OpenSearchSearchHit) => boolean;
+ title: string;
+}
+
+export interface DocView extends DocViewInput {
+ shouldShow: (hit: OpenSearchSearchHit) => boolean;
+}
+
+export type DocViewInputFn = () => DocViewInput;
diff --git a/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts
new file mode 100644
index 000000000000..16653f5d5377
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_registry.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { DocViewLink } from './doc_views_links_types';
+
+export class DocViewsLinksRegistry {
+ private docViewsLinks: DocViewLink[] = [];
+
+ addDocViewLink(docViewLink: DocViewLink) {
+ this.docViewsLinks.push(docViewLink);
+ }
+
+ getDocViewsLinksSorted() {
+ return this.docViewsLinks.sort((a, b) => (Number(a.order) > Number(b.order) ? 1 : -1));
+ }
+}
diff --git a/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts
new file mode 100644
index 000000000000..bbc5caadafcd
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/doc_views_links/doc_views_links_types.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { EuiListGroupItemProps } from '@elastic/eui';
+import { OpenSearchSearchHit } from '../doc_views/doc_views_types';
+import { IndexPattern } from '../../../../data/public';
+
+export interface DocViewLink extends EuiListGroupItemProps {
+ href?: string;
+ order: number;
+ generateCb?(
+ renderProps: any
+ ): {
+ url: string;
+ hide?: boolean;
+ };
+}
+
+export interface DocViewLinkRenderProps {
+ columns?: string[];
+ hit: OpenSearchSearchHit;
+ indexPattern: IndexPattern;
+}
diff --git a/src/plugins/discover/public/application/embeddable/constants.ts b/src/plugins/discover_legacy/public/application/embeddable/constants.ts
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/constants.ts
rename to src/plugins/discover_legacy/public/application/embeddable/constants.ts
diff --git a/src/plugins/discover/public/application/embeddable/index.ts b/src/plugins/discover_legacy/public/application/embeddable/index.ts
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/index.ts
rename to src/plugins/discover_legacy/public/application/embeddable/index.ts
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.scss b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable.scss
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/search_embeddable.scss
rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable.scss
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable.ts
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/search_embeddable.ts
rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable.ts
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover_legacy/public/application/embeddable/search_embeddable_factory.ts
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts
rename to src/plugins/discover_legacy/public/application/embeddable/search_embeddable_factory.ts
diff --git a/src/plugins/discover/public/application/embeddable/search_template.html b/src/plugins/discover_legacy/public/application/embeddable/search_template.html
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/search_template.html
rename to src/plugins/discover_legacy/public/application/embeddable/search_template.html
diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover_legacy/public/application/embeddable/types.ts
similarity index 100%
rename from src/plugins/discover/public/application/embeddable/types.ts
rename to src/plugins/discover_legacy/public/application/embeddable/types.ts
diff --git a/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts b/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts
new file mode 100644
index 000000000000..e30f50206aef
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/breadcrumbs.ts
@@ -0,0 +1,51 @@
+/*
+ * 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';
+
+export function getRootBreadcrumbs() {
+ return [
+ {
+ text: i18n.translate('discover.rootBreadcrumb', {
+ defaultMessage: 'Discover',
+ }),
+ href: '#/',
+ },
+ ];
+}
+
+export function getSavedSearchBreadcrumbs($route: any) {
+ return [
+ ...getRootBreadcrumbs(),
+ {
+ text: $route.current.locals.savedObjects.savedSearch.id,
+ },
+ ];
+}
diff --git a/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts b/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts
new file mode 100644
index 000000000000..b1b3c96e0958
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/format_number_with_commas.ts
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+const COMMA_SEPARATOR_RE = /(\d)(?=(\d{3})+(?!\d))/g;
+
+/**
+ * Converts a number to a string and adds commas
+ * as thousands separators
+ */
+export const formatNumWithCommas = (input: number) =>
+ String(input).replace(COMMA_SEPARATOR_RE, '$1,');
diff --git a/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts b/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts
new file mode 100644
index 000000000000..dfb02c0b0740
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/get_index_pattern_id.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { IIndexPattern } from '../../../../data/common/index_patterns';
+
+export function findIndexPatternById(
+ indexPatterns: IIndexPattern[],
+ id: string
+): IIndexPattern | undefined {
+ if (!Array.isArray(indexPatterns) || !id) {
+ return;
+ }
+ return indexPatterns.find((o) => o.id === id);
+}
+
+/**
+ * Checks if the given defaultIndex exists and returns
+ * the first available index pattern id if not
+ */
+export function getFallbackIndexPatternId(
+ indexPatterns: IIndexPattern[],
+ defaultIndex: string = ''
+): string {
+ if (defaultIndex && findIndexPatternById(indexPatterns, defaultIndex)) {
+ return defaultIndex;
+ }
+ return !indexPatterns || !indexPatterns.length || !indexPatterns[0].id ? '' : indexPatterns[0].id;
+}
+
+/**
+ * A given index pattern id is checked for existence and a fallback is provided if it doesn't exist
+ * The provided defaultIndex is usually configured in Advanced Settings, if it's also invalid
+ * the first entry of the given list of Indexpatterns is used
+ */
+export function getIndexPatternId(
+ id: string = '',
+ indexPatterns: IIndexPattern[],
+ defaultIndex: string = ''
+): string {
+ if (!id || !findIndexPatternById(indexPatterns, id)) {
+ return getFallbackIndexPatternId(indexPatterns, defaultIndex);
+ }
+ return id;
+}
diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts b/src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.test.ts
similarity index 100%
rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts
rename to src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.test.ts
diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts b/src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.ts
similarity index 100%
rename from src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts
rename to src/plugins/discover_legacy/public/application/helpers/get_switch_index_pattern_app_state.ts
diff --git a/src/plugins/discover_legacy/public/application/helpers/index.ts b/src/plugins/discover_legacy/public/application/helpers/index.ts
new file mode 100644
index 000000000000..d765fdf60cee
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/index.ts
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+export { shortenDottedString } from './shorten_dotted_string';
+export { formatNumWithCommas } from './format_number_with_commas';
diff --git a/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts
new file mode 100644
index 000000000000..90458c135b98
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/migrate_legacy_query.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { has } from 'lodash';
+import { Query } from 'src/plugins/data/public';
+
+/**
+ * Creates a standardized query object from old queries that were either strings or pure OpenSearch query DSL
+ *
+ * @param query - a legacy query, what used to be stored in SearchSource's query property
+ * @return Object
+ */
+
+export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query {
+ // Lucene was the only option before, so language-less queries are all lucene
+ if (!has(query, 'language')) {
+ return { query, language: 'lucene' };
+ }
+
+ return query as Query;
+}
diff --git a/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts b/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts
new file mode 100644
index 000000000000..cdd49c0f77f1
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/popularize_field.test.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { IndexPattern, IndexPatternsService } from '../../../../data/public';
+import { popularizeField } from './popularize_field';
+
+describe('Popularize field', () => {
+ test('returns undefined if index pattern lacks id', async () => {
+ const indexPattern = ({} as unknown) as IndexPattern;
+ const fieldName = '@timestamp';
+ const indexPatternsService = ({} as unknown) as IndexPatternsService;
+ const result = await popularizeField(indexPattern, fieldName, indexPatternsService);
+ expect(result).toBeUndefined();
+ });
+
+ test('returns undefined if field not found', async () => {
+ const indexPattern = ({
+ fields: {
+ getByName: () => {},
+ },
+ } as unknown) as IndexPattern;
+ const fieldName = '@timestamp';
+ const indexPatternsService = ({} as unknown) as IndexPatternsService;
+ const result = await popularizeField(indexPattern, fieldName, indexPatternsService);
+ expect(result).toBeUndefined();
+ });
+
+ test('returns undefined if successful', async () => {
+ const field = {
+ count: 0,
+ };
+ const indexPattern = ({
+ id: 'id',
+ fields: {
+ getByName: () => field,
+ },
+ } as unknown) as IndexPattern;
+ const fieldName = '@timestamp';
+ const indexPatternsService = ({
+ updateSavedObject: async () => {},
+ } as unknown) as IndexPatternsService;
+ const result = await popularizeField(indexPattern, fieldName, indexPatternsService);
+ expect(result).toBeUndefined();
+ expect(field.count).toEqual(1);
+ });
+
+ test('hides errors', async () => {
+ const field = {
+ count: 0,
+ };
+ const indexPattern = ({
+ id: 'id',
+ fields: {
+ getByName: () => field,
+ },
+ } as unknown) as IndexPattern;
+ const fieldName = '@timestamp';
+ const indexPatternsService = ({
+ updateSavedObject: async () => {
+ throw new Error('unknown error');
+ },
+ } as unknown) as IndexPatternsService;
+ const result = await popularizeField(indexPattern, fieldName, indexPatternsService);
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts b/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts
new file mode 100644
index 000000000000..e7c4b900fa19
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/popularize_field.ts
@@ -0,0 +1,52 @@
+/*
+ * 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 { IndexPattern, IndexPatternsService } from '../../../../data/public';
+
+async function popularizeField(
+ indexPattern: IndexPattern,
+ fieldName: string,
+ indexPatternsService: IndexPatternsService
+) {
+ if (!indexPattern.id) return;
+ const field = indexPattern.fields.getByName(fieldName);
+ if (!field) {
+ return;
+ }
+
+ field.count++;
+ // Catch 409 errors caused by user adding columns in a higher frequency that the changes can be persisted to OpenSearch
+ try {
+ await indexPatternsService.updateSavedObject(indexPattern, 0, true);
+ // eslint-disable-next-line no-empty
+ } catch {}
+}
+
+export { popularizeField };
diff --git a/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts b/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts
new file mode 100644
index 000000000000..39450f8c82c0
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/shorten_dotted_string.ts
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+const DOT_PREFIX_RE = /(.).+?\./g;
+
+/**
+ * Convert a dot.notated.string into a short
+ * version (d.n.string)
+ */
+export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.');
diff --git a/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts
new file mode 100644
index 000000000000..902f3d8a4b62
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.test.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { validateTimeRange } from './validate_time_range';
+import { notificationServiceMock } from '../../../../../core/public/mocks';
+
+describe('Discover validateTimeRange', () => {
+ test('validates given time ranges correctly', async () => {
+ const { toasts } = notificationServiceMock.createStartContract();
+ [
+ { from: '', to: '', result: false },
+ { from: 'now', to: 'now+1h', result: true },
+ { from: 'now', to: 'lala+1h', result: false },
+ { from: '', to: 'now', result: false },
+ { from: 'now', to: '', result: false },
+ { from: ' 2020-06-02T13:36:13.689Z', to: 'now', result: true },
+ { from: ' 2020-06-02T13:36:13.689Z', to: '2020-06-02T13:36:13.690Z', result: true },
+ ].map((test) => {
+ expect(validateTimeRange({ from: test.from, to: test.to }, toasts)).toEqual(test.result);
+ });
+ });
+
+ test('displays a toast when invalid data is entered', async () => {
+ const { toasts } = notificationServiceMock.createStartContract();
+ expect(validateTimeRange({ from: 'now', to: 'null' }, toasts)).toEqual(false);
+ expect(toasts.addDanger).toHaveBeenCalledWith({
+ title: 'Invalid time range',
+ text: "The provided time range is invalid. (from: 'now', to: 'null')",
+ });
+ });
+});
diff --git a/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts
new file mode 100644
index 000000000000..d23a84aabb14
--- /dev/null
+++ b/src/plugins/discover_legacy/public/application/helpers/validate_time_range.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 dateMath from '@elastic/datemath';
+import { i18n } from '@osd/i18n';
+import { ToastsStart } from 'opensearch-dashboards/public';
+
+/**
+ * Validates a given time filter range, provided by URL or UI
+ * Unless valid, it returns false and displays a notification
+ */
+export function validateTimeRange(
+ { from, to }: { from: string; to: string },
+ toastNotifications: ToastsStart
+): boolean {
+ const fromMoment = dateMath.parse(from);
+ const toMoment = dateMath.parse(to);
+ if (!fromMoment || !toMoment || !fromMoment.isValid() || !toMoment.isValid()) {
+ toastNotifications.addDanger({
+ title: i18n.translate('discover.notifications.invalidTimeRangeTitle', {
+ defaultMessage: `Invalid time range`,
+ }),
+ text: i18n.translate('discover.notifications.invalidTimeRangeText', {
+ defaultMessage: `The provided time range is invalid. (from: '{from}', to: '{to}')`,
+ values: {
+ from,
+ to,
+ },
+ }),
+ });
+ return false;
+ }
+ return true;
+}
diff --git a/src/plugins/discover/public/application/index.scss b/src/plugins/discover_legacy/public/application/index.scss
similarity index 100%
rename from src/plugins/discover/public/application/index.scss
rename to src/plugins/discover_legacy/public/application/index.scss
diff --git a/src/plugins/discover_legacy/public/build_services.ts b/src/plugins/discover_legacy/public/build_services.ts
new file mode 100644
index 000000000000..3fdafcff0c40
--- /dev/null
+++ b/src/plugins/discover_legacy/public/build_services.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 { History } from 'history';
+
+import {
+ Capabilities,
+ ChromeStart,
+ CoreStart,
+ DocLinksStart,
+ ToastsStart,
+ IUiSettingsClient,
+ PluginInitializerContext,
+} from 'opensearch-dashboards/public';
+import {
+ FilterManager,
+ TimefilterContract,
+ IndexPatternsContract,
+ DataPublicPluginStart,
+} from 'src/plugins/data/public';
+import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public';
+import { SharePluginStart } from 'src/plugins/share/public';
+import { ChartsPluginStart } from 'src/plugins/charts/public';
+import { VisualizationsStart } from 'src/plugins/visualizations/public';
+import { SavedObjectOpenSearchDashboardsServices } from 'src/plugins/saved_objects/public';
+
+import { DiscoverStartPlugins } from './plugin';
+import { createSavedSearchesLoader, SavedSearch } from './saved_searches';
+import { getHistory } from './opensearch_dashboards_services';
+import { OpenSearchDashboardsLegacyStart } from '../../opensearch_dashboards_legacy/public';
+import { UrlForwardingStart } from '../../url_forwarding/public';
+import { NavigationPublicPluginStart } from '../../navigation/public';
+
+export interface DiscoverServices {
+ addBasePath: (path: string) => string;
+ capabilities: Capabilities;
+ chrome: ChromeStart;
+ core: CoreStart;
+ data: DataPublicPluginStart;
+ docLinks: DocLinksStart;
+ history: () => History;
+ theme: ChartsPluginStart['theme'];
+ filterManager: FilterManager;
+ indexPatterns: IndexPatternsContract;
+ inspector: InspectorPublicPluginStart;
+ metadata: { branch: string };
+ navigation: NavigationPublicPluginStart;
+ share?: SharePluginStart;
+ opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart;
+ urlForwarding: UrlForwardingStart;
+ timefilter: TimefilterContract;
+ toastNotifications: ToastsStart;
+ getSavedSearchById: (id: string) => Promise;
+ getSavedSearchUrlById: (id: string) => Promise;
+ getEmbeddableInjector: any;
+ uiSettings: IUiSettingsClient;
+ visualizations: VisualizationsStart;
+}
+
+export async function buildServices(
+ core: CoreStart,
+ plugins: DiscoverStartPlugins,
+ context: PluginInitializerContext,
+ getEmbeddableInjector: any
+): Promise {
+ const services: SavedObjectOpenSearchDashboardsServices = {
+ savedObjectsClient: core.savedObjects.client,
+ indexPatterns: plugins.data.indexPatterns,
+ search: plugins.data.search,
+ chrome: core.chrome,
+ overlays: core.overlays,
+ };
+ const savedObjectService = createSavedSearchesLoader(services);
+
+ return {
+ addBasePath: core.http.basePath.prepend,
+ capabilities: core.application.capabilities,
+ chrome: core.chrome,
+ core,
+ data: plugins.data,
+ docLinks: core.docLinks,
+ theme: plugins.charts.theme,
+ filterManager: plugins.data.query.filterManager,
+ getEmbeddableInjector,
+ getSavedSearchById: async (id: string) => savedObjectService.get(id),
+ getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id),
+ history: getHistory,
+ indexPatterns: plugins.data.indexPatterns,
+ inspector: plugins.inspector,
+ metadata: {
+ branch: context.env.packageInfo.branch,
+ },
+ navigation: plugins.navigation,
+ share: plugins.share,
+ opensearchDashboardsLegacy: plugins.opensearchDashboardsLegacy,
+ urlForwarding: plugins.urlForwarding,
+ timefilter: plugins.data.query.timefilter.timefilter,
+ toastNotifications: core.notifications.toasts,
+ uiSettings: core.uiSettings,
+ visualizations: plugins.visualizations,
+ };
+}
diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover_legacy/public/get_inner_angular.ts
similarity index 100%
rename from src/plugins/discover/public/get_inner_angular.ts
rename to src/plugins/discover_legacy/public/get_inner_angular.ts
diff --git a/src/plugins/discover_legacy/public/index.ts b/src/plugins/discover_legacy/public/index.ts
new file mode 100644
index 000000000000..6c9ab46b656e
--- /dev/null
+++ b/src/plugins/discover_legacy/public/index.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { PluginInitializerContext } from 'opensearch-dashboards/public';
+import { DiscoverPlugin } from './plugin';
+
+export { DiscoverSetup, DiscoverStart } from './plugin';
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new DiscoverPlugin(initializerContext);
+}
+
+export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches';
+export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable';
+export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator';
diff --git a/src/plugins/discover_legacy/public/mocks.ts b/src/plugins/discover_legacy/public/mocks.ts
new file mode 100644
index 000000000000..4724ced290ff
--- /dev/null
+++ b/src/plugins/discover_legacy/public/mocks.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { DiscoverSetup, DiscoverStart } from '.';
+
+export type Setup = jest.Mocked;
+export type Start = jest.Mocked;
+
+const createSetupContract = (): Setup => {
+ const setupContract: Setup = {
+ docViews: {
+ addDocView: jest.fn(),
+ },
+ docViewsLinks: {
+ addDocViewLink: jest.fn(),
+ },
+ };
+ return setupContract;
+};
+
+const createStartContract = (): Start => {
+ const startContract: Start = {
+ savedSearchLoader: {} as any,
+ urlGenerator: {
+ createUrl: jest.fn(),
+ } as any,
+ };
+ return startContract;
+};
+
+export const discoverPluginMock = {
+ createSetupContract,
+ createStartContract,
+};
diff --git a/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts b/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts
new file mode 100644
index 000000000000..8531564e0cc7
--- /dev/null
+++ b/src/plugins/discover_legacy/public/opensearch_dashboards_services.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 _ from 'lodash';
+import { createHashHistory } from 'history';
+import { ScopedHistory, AppMountParameters } from 'opensearch-dashboards/public';
+import { UiActionsStart } from 'src/plugins/ui_actions/public';
+import { DiscoverServices } from './build_services';
+import { createGetterSetter } from '../../opensearch_dashboards_utils/public';
+import { search } from '../../data/public';
+import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
+import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry';
+
+let angularModule: any = null;
+let services: DiscoverServices | null = null;
+let uiActions: UiActionsStart;
+
+/**
+ * set bootstrapped inner angular module
+ */
+export function setAngularModule(module: any) {
+ angularModule = module;
+}
+
+/**
+ * get boostrapped inner angular module
+ */
+export function getAngularModule() {
+ return angularModule;
+}
+
+export function getServices(): DiscoverServices {
+ if (!services) {
+ throw new Error('Discover services are not yet available');
+ }
+ return services;
+}
+
+export function setServices(newServices: any) {
+ services = newServices;
+}
+
+export const setUiActions = (pluginUiActions: UiActionsStart) => (uiActions = pluginUiActions);
+export const getUiActions = () => uiActions;
+
+export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter<
+ AppMountParameters['setHeaderActionMenu']
+>('headerActionMenuMounter');
+
+export const [getUrlTracker, setUrlTracker] = createGetterSetter<{
+ setTrackedUrl: (url: string) => void;
+ restorePreviousUrl: () => void;
+}>('urlTracker');
+
+export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter(
+ 'DocViewsRegistry'
+);
+
+export const [getDocViewsLinksRegistry, setDocViewsLinksRegistry] = createGetterSetter<
+ DocViewsLinksRegistry
+>('DocViewsLinksRegistry');
+/**
+ * Makes sure discover and context are using one instance of history.
+ */
+export const getHistory = _.once(() => createHashHistory());
+
+/**
+ * Discover currently uses two `history` instances: one from OpenSearch Dashboards Platform and
+ * another from `history` package. Below function is used every time Discover
+ * app is loaded to synchronize both instances.
+ *
+ * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved.
+ */
+export const syncHistoryLocations = () => {
+ const h = getHistory();
+ Object.assign(h.location, createHashHistory().location);
+ return h;
+};
+
+export const [getScopedHistory, setScopedHistory] = createGetterSetter(
+ 'scopedHistory'
+);
+
+export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search;
+export { unhashUrl, redirectWhenMissing } from '../../opensearch_dashboards_utils/public';
+export {
+ formatMsg,
+ formatStack,
+ subscribeWithScope,
+} from '../../opensearch_dashboards_legacy/public';
+
+// EXPORT types
+export {
+ IndexPatternsContract,
+ IIndexPattern,
+ IndexPattern,
+ indexPatterns,
+ IFieldType,
+ ISearchSource,
+ OpenSearchQuerySortValue,
+ SortDirection,
+} from '../../data/public';
diff --git a/src/plugins/discover_legacy/public/plugin.ts b/src/plugins/discover_legacy/public/plugin.ts
new file mode 100644
index 000000000000..7e855b707891
--- /dev/null
+++ b/src/plugins/discover_legacy/public/plugin.ts
@@ -0,0 +1,487 @@
+/*
+ * 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 angular, { auto } from 'angular';
+import { BehaviorSubject } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+
+import {
+ AppMountParameters,
+ AppUpdater,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ PluginInitializerContext,
+} from 'opensearch-dashboards/public';
+import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public';
+import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public';
+import { ChartsPluginStart } from 'src/plugins/charts/public';
+import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
+import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public';
+import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public';
+import {
+ OpenSearchDashboardsLegacySetup,
+ OpenSearchDashboardsLegacyStart,
+} from 'src/plugins/opensearch_dashboards_legacy/public';
+import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public';
+import { HomePublicPluginSetup } from 'src/plugins/home/public';
+import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public';
+import { stringify } from 'query-string';
+import rison from 'rison-node';
+import { NEW_DISCOVER_APP } from '../../discover/public';
+import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public';
+import { SavedObjectLoader } from '../../saved_objects/public';
+import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public';
+import { UrlGeneratorState } from '../../share/public';
+import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types';
+import { DocViewLink } from './application/doc_views_links/doc_views_links_types';
+import { DocViewsRegistry } from './application/doc_views/doc_views_registry';
+import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry';
+import { DocViewTable } from './application/components/table/table';
+import { JsonCodeBlock } from './application/components/json_code_block/json_code_block';
+import {
+ setDocViewsRegistry,
+ setDocViewsLinksRegistry,
+ setUrlTracker,
+ setAngularModule,
+ setServices,
+ setHeaderActionMenuMounter,
+ setUiActions,
+ setScopedHistory,
+ getScopedHistory,
+ syncHistoryLocations,
+ getServices,
+} from './opensearch_dashboards_services';
+import { createSavedSearchesLoader } from './saved_searches';
+import { buildServices } from './build_services';
+import {
+ DiscoverUrlGeneratorState,
+ DISCOVER_APP_URL_GENERATOR,
+ DiscoverUrlGenerator,
+} from './url_generator';
+import { SearchEmbeddableFactory } from './application/embeddable';
+import { AppNavLinkStatus } from '../../../core/public';
+import { ViewRedirectParams } from '../../data_explorer/public';
+
+declare module '../../share/public' {
+ export interface UrlGeneratorStateMapping {
+ [DISCOVER_APP_URL_GENERATOR]: UrlGeneratorState;
+ }
+}
+
+/**
+ * @public
+ */
+export interface DiscoverSetup {
+ docViews: {
+ /**
+ * Add new doc view shown along with table view and json view in the details of each document in Discover.
+ * Both react and angular doc views are supported.
+ * @param docViewRaw
+ */
+ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void;
+ };
+
+ docViewsLinks: {
+ addDocViewLink(docViewLinkRaw: DocViewLink): void;
+ };
+}
+
+export interface DiscoverStart {
+ savedSearchLoader: SavedObjectLoader;
+
+ /**
+ * `share` plugin URL generator for Discover app. Use it to generate links into
+ * Discover application, example:
+ *
+ * ```ts
+ * const url = await plugins.discover.urlGenerator.createUrl({
+ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d',
+ * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002',
+ * timeRange: {
+ * to: 'now',
+ * from: 'now-15m',
+ * mode: 'relative',
+ * },
+ * });
+ * ```
+ */
+ readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>;
+}
+
+/**
+ * @internal
+ */
+export interface DiscoverSetupPlugins {
+ share?: SharePluginSetup;
+ uiActions: UiActionsSetup;
+ embeddable: EmbeddableSetup;
+ opensearchDashboardsLegacy: OpenSearchDashboardsLegacySetup;
+ urlForwarding: UrlForwardingSetup;
+ home?: HomePublicPluginSetup;
+ visualizations: VisualizationsSetup;
+ data: DataPublicPluginSetup;
+}
+
+/**
+ * @internal
+ */
+export interface DiscoverStartPlugins {
+ uiActions: UiActionsStart;
+ embeddable: EmbeddableStart;
+ navigation: NavigationStart;
+ charts: ChartsPluginStart;
+ data: DataPublicPluginStart;
+ share?: SharePluginStart;
+ opensearchDashboardsLegacy: OpenSearchDashboardsLegacyStart;
+ urlForwarding: UrlForwardingStart;
+ inspector: InspectorPublicPluginStart;
+ visualizations: VisualizationsStart;
+}
+
+const innerAngularName = 'app/discover';
+const embeddableAngularName = 'app/discoverEmbeddable';
+
+/**
+ * Contains Discover, one of the oldest parts of OpenSearch Dashboards
+ * There are 2 kinds of Angular bootstrapped for rendering, additionally to the main Angular
+ * Discover provides embeddables, those contain a slimmer Angular
+ */
+export class DiscoverPlugin
+ implements Plugin {
+ constructor(private readonly initializerContext: PluginInitializerContext) {}
+
+ private appStateUpdater = new BehaviorSubject(() => ({}));
+ private docViewsRegistry: DocViewsRegistry | null = null;
+ private docViewsLinksRegistry: DocViewsLinksRegistry | null = null;
+ private embeddableInjector: auto.IInjectorService | null = null;
+ private stopUrlTracking: (() => void) | undefined = undefined;
+ private servicesInitialized: boolean = false;
+ private innerAngularInitialized: boolean = false;
+ private urlGenerator?: DiscoverStart['urlGenerator'];
+
+ /**
+ * why are those functions public? they are needed for some mocha tests
+ * can be removed once all is Jest
+ */
+ public initializeInnerAngular?: () => void;
+ public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>;
+
+ setup(core: CoreSetup, plugins: DiscoverSetupPlugins) {
+ const baseUrl = core.http.basePath.prepend('/app/discover');
+
+ if (plugins.share) {
+ this.urlGenerator = plugins.share.urlGenerators.registerUrlGenerator(
+ new DiscoverUrlGenerator({
+ appBasePath: baseUrl,
+ useHash: core.uiSettings.get('state:storeInSessionStorage'),
+ })
+ );
+ }
+
+ this.docViewsRegistry = new DocViewsRegistry();
+ setDocViewsRegistry(this.docViewsRegistry);
+ this.docViewsRegistry.addDocView({
+ title: i18n.translate('discover.docViews.table.tableTitle', {
+ defaultMessage: 'Table',
+ }),
+ order: 10,
+ component: DocViewTable,
+ });
+
+ this.docViewsRegistry.addDocView({
+ title: i18n.translate('discover.docViews.json.jsonTitle', {
+ defaultMessage: 'JSON',
+ }),
+ order: 20,
+ component: JsonCodeBlock,
+ });
+
+ this.docViewsLinksRegistry = new DocViewsLinksRegistry();
+ setDocViewsLinksRegistry(this.docViewsLinksRegistry);
+
+ this.docViewsLinksRegistry.addDocViewLink({
+ label: i18n.translate('discover.docTable.tableRow.viewSurroundingDocumentsLinkText', {
+ defaultMessage: 'View surrounding documents',
+ }),
+ generateCb: (renderProps: any) => {
+ const globalFilters: any = getServices().filterManager.getGlobalFilters();
+ const appFilters: any = getServices().filterManager.getAppFilters();
+
+ const hash = stringify(
+ url.encodeQuery({
+ _g: rison.encode({
+ filters: globalFilters || [],
+ }),
+ _a: rison.encode({
+ columns: renderProps.columns,
+ filters: (appFilters || []).map(opensearchFilters.disableFilter),
+ }),
+ }),
+ { encode: false, sort: false }
+ );
+
+ return {
+ url: `#/context/${encodeURIComponent(renderProps.indexPattern.id)}/${encodeURIComponent(
+ renderProps.hit._id
+ )}?${hash}`,
+ hide: !renderProps.indexPattern.isTimeBased(),
+ };
+ },
+ order: 1,
+ });
+
+ this.docViewsLinksRegistry.addDocViewLink({
+ label: i18n.translate('discover.docTable.tableRow.viewSingleDocumentLinkText', {
+ defaultMessage: 'View single document',
+ }),
+ generateCb: (renderProps) => ({
+ url: `#/doc/${renderProps.indexPattern.id}/${
+ renderProps.hit._index
+ }?id=${encodeURIComponent(renderProps.hit._id)}`,
+ }),
+ order: 2,
+ });
+
+ const {
+ appMounted,
+ appUnMounted,
+ stop: stopUrlTracker,
+ setActiveUrl: setTrackedUrl,
+ restorePreviousUrl,
+ } = createOsdUrlTracker({
+ // we pass getter here instead of plain `history`,
+ // so history is lazily created (when app is mounted)
+ // this prevents redundant `#` when not in discover app
+ getHistory: getScopedHistory,
+ baseUrl,
+ defaultSubUrl: '#/',
+ storageKey: `lastUrl:${core.http.basePath.get()}:discover_legacy`,
+ navLinkUpdater$: this.appStateUpdater,
+ toastNotifications: core.notifications.toasts,
+ stateParams: [
+ {
+ osdUrlKey: '_g',
+ stateUpdate$: plugins.data.query.state$.pipe(
+ filter(
+ ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval)
+ ),
+ map(({ state }) => ({
+ ...state,
+ filters: state.filters?.filter(opensearchFilters.isFilterPinned),
+ }))
+ ),
+ },
+ ],
+ });
+ setUrlTracker({ setTrackedUrl, restorePreviousUrl });
+ this.stopUrlTracking = () => {
+ stopUrlTracker();
+ };
+
+ this.docViewsRegistry.setAngularInjectorGetter(this.getEmbeddableInjector);
+ core.application.register({
+ id: 'discoverLegacy',
+ title: 'Discover Legacy',
+ defaultPath: '#/',
+ navLinkStatus: AppNavLinkStatus.hidden,
+ mount: async (params: AppMountParameters) => {
+ if (!this.initializeServices) {
+ throw Error('Discover plugin method initializeServices is undefined');
+ }
+ if (!this.initializeInnerAngular) {
+ throw Error('Discover plugin method initializeInnerAngular is undefined');
+ }
+
+ // If a user explicitly tries to access the legacy app URL
+ const {
+ core: {
+ application: { navigateToApp },
+ },
+ } = await this.initializeServices();
+ const path = window.location.hash;
+
+ const v2Enabled = core.uiSettings.get(NEW_DISCOVER_APP);
+ if (v2Enabled) {
+ navigateToApp('discover', {
+ replace: true,
+ path,
+ });
+ }
+ setScopedHistory(params.history);
+ setHeaderActionMenuMounter(params.setHeaderActionMenu);
+ syncHistoryLocations();
+ appMounted();
+ const {
+ plugins: { data: dataStart },
+ } = await this.initializeServices();
+ await this.initializeInnerAngular();
+
+ // make sure the index pattern list is up to date
+ await dataStart.indexPatterns.clearCache();
+ const { renderApp } = await import('./application/application');
+ params.element.classList.add('dscAppWrapper');
+ const unmount = await renderApp(innerAngularName, params.element);
+ return () => {
+ params.element.classList.remove('dscAppWrapper');
+ unmount();
+ appUnMounted();
+ };
+ },
+ });
+
+ plugins.urlForwarding.forwardApp('doc', 'discoverLegacy', (path) => {
+ return `#${path}`;
+ });
+ plugins.urlForwarding.forwardApp('context', 'discoverLegacy', (path) => {
+ const urlParts = path.split('/');
+ // take care of urls containing legacy url, those split in the following way
+ // ["", "context", indexPatternId, _type, id + params]
+ if (urlParts[4]) {
+ // remove _type part
+ const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/');
+ return `#${newPath}`;
+ }
+ return `#${path}`;
+ });
+ plugins.urlForwarding.forwardApp('discover', 'discoverLegacy', (path) => {
+ const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || [];
+ if (!id) {
+ return `#${path.replace('/discover', '') || '/'}`;
+ }
+ return `#/view/${id}${tail || ''}`;
+ });
+
+ this.registerEmbeddable(core, plugins);
+
+ return {
+ docViews: {
+ addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry),
+ },
+ docViewsLinks: {
+ addDocViewLink: this.docViewsLinksRegistry.addDocViewLink.bind(this.docViewsLinksRegistry),
+ },
+ };
+ }
+
+ start(core: CoreStart, plugins: DiscoverStartPlugins) {
+ // we need to register the application service at setup, but to render it
+ // there are some start dependencies necessary, for this reason
+ // initializeInnerAngular + initializeServices are assigned at start and used
+ // when the application/embeddable is mounted
+ this.initializeInnerAngular = async () => {
+ if (this.innerAngularInitialized) {
+ return;
+ }
+ // this is used by application mount and tests
+ const { getInnerAngularModule } = await import('./get_inner_angular');
+ const module = getInnerAngularModule(
+ innerAngularName,
+ core,
+ plugins,
+ this.initializerContext
+ );
+ setAngularModule(module);
+ this.innerAngularInitialized = true;
+ };
+
+ setUiActions(plugins.uiActions);
+
+ this.initializeServices = async () => {
+ if (this.servicesInitialized) {
+ return { core, plugins };
+ }
+ const services = await buildServices(
+ core,
+ plugins,
+ this.initializerContext,
+ this.getEmbeddableInjector
+ );
+ setServices(services);
+ this.servicesInitialized = true;
+
+ return { core, plugins };
+ };
+
+ return {
+ urlGenerator: this.urlGenerator,
+ savedSearchLoader: createSavedSearchesLoader({
+ savedObjectsClient: core.savedObjects.client,
+ indexPatterns: plugins.data.indexPatterns,
+ search: plugins.data.search,
+ chrome: core.chrome,
+ overlays: core.overlays,
+ }),
+ };
+ }
+
+ stop() {
+ if (this.stopUrlTracking) {
+ this.stopUrlTracking();
+ }
+ }
+
+ /**
+ * register embeddable with a slimmer embeddable version of inner angular
+ */
+ private registerEmbeddable(core: CoreSetup, plugins: DiscoverSetupPlugins) {
+ if (!this.getEmbeddableInjector) {
+ throw Error('Discover plugin method getEmbeddableInjector is undefined');
+ }
+
+ const getStartServices = async () => {
+ const [coreStart, deps] = await core.getStartServices();
+ return {
+ executeTriggerActions: deps.uiActions.executeTriggerActions,
+ isEditable: () => coreStart.application.capabilities.discover.save as boolean,
+ };
+ };
+
+ const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector);
+ plugins.embeddable.registerEmbeddableFactory(factory.type, factory);
+ }
+
+ private getEmbeddableInjector = async () => {
+ if (!this.embeddableInjector) {
+ if (!this.initializeServices) {
+ throw Error('Discover plugin getEmbeddableInjector: initializeServices is undefined');
+ }
+ const { core, plugins } = await this.initializeServices();
+ getServices().opensearchDashboardsLegacy.loadFontAwesome();
+ const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular');
+ getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins);
+ const mountpoint = document.createElement('div');
+ this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]);
+ }
+
+ return this.embeddableInjector;
+ };
+}
diff --git a/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts b/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts
new file mode 100644
index 000000000000..55cd59104ecb
--- /dev/null
+++ b/src/plugins/discover_legacy/public/saved_searches/_saved_search.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {
+ createSavedObjectClass,
+ SavedObject,
+ SavedObjectOpenSearchDashboardsServices,
+} from '../../../saved_objects/public';
+
+export function createSavedSearchClass(services: SavedObjectOpenSearchDashboardsServices) {
+ const SavedObjectClass = createSavedObjectClass(services);
+
+ class SavedSearch extends SavedObjectClass {
+ public static type: string = 'search';
+ public static mapping = {
+ title: 'text',
+ description: 'text',
+ hits: 'integer',
+ columns: 'keyword',
+ sort: 'keyword',
+ version: 'integer',
+ };
+ // Order these fields to the top, the rest are alphabetical
+ public static fieldOrder = ['title', 'description'];
+ public static searchSource = true;
+
+ public id: string;
+ public showInRecentlyAccessed: boolean;
+
+ constructor(id: string) {
+ super({
+ id,
+ type: 'search',
+ mapping: {
+ title: 'text',
+ description: 'text',
+ hits: 'integer',
+ columns: 'keyword',
+ sort: 'keyword',
+ version: 'integer',
+ },
+ searchSource: true,
+ defaults: {
+ title: '',
+ description: '',
+ columns: [],
+ hits: 0,
+ sort: [],
+ version: 1,
+ },
+ });
+ this.showInRecentlyAccessed = true;
+ this.id = id;
+ this.getFullPath = () => `/app/discover#/view/${String(id)}`;
+ }
+ }
+
+ return SavedSearch as new (id: string) => SavedObject;
+}
diff --git a/src/plugins/discover_legacy/public/saved_searches/index.ts b/src/plugins/discover_legacy/public/saved_searches/index.ts
new file mode 100644
index 000000000000..f576a9a9377a
--- /dev/null
+++ b/src/plugins/discover_legacy/public/saved_searches/index.ts
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+export { createSavedSearchesLoader } from './saved_searches';
+export { SavedSearch, SavedSearchLoader } from './types';
diff --git a/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts b/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts
new file mode 100644
index 000000000000..dd3243568159
--- /dev/null
+++ b/src/plugins/discover_legacy/public/saved_searches/saved_searches.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 {
+ SavedObjectLoader,
+ SavedObjectOpenSearchDashboardsServices,
+} from '../../../saved_objects/public';
+import { createSavedSearchClass } from './_saved_search';
+
+export function createSavedSearchesLoader(services: SavedObjectOpenSearchDashboardsServices) {
+ const SavedSearchClass = createSavedSearchClass(services);
+ const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient);
+ // Customize loader properties since adding an 's' on type doesn't work for type 'search' .
+ savedSearchLoader.loaderProperties = {
+ name: 'searches',
+ noun: 'Saved Search',
+ nouns: 'saved searches',
+ };
+
+ savedSearchLoader.urlFor = (id: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/');
+
+ return savedSearchLoader;
+}
diff --git a/src/plugins/discover_legacy/public/saved_searches/types.ts b/src/plugins/discover_legacy/public/saved_searches/types.ts
new file mode 100644
index 000000000000..e02fd65e6899
--- /dev/null
+++ b/src/plugins/discover_legacy/public/saved_searches/types.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { ISearchSource } from '../../../data/public';
+
+export type SortOrder = [string, string];
+export interface SavedSearch {
+ readonly id: string;
+ title: string;
+ searchSource: ISearchSource;
+ description?: string;
+ columns: string[];
+ sort: SortOrder[];
+ destroy: () => void;
+ lastSavedTitle?: string;
+}
+export interface SavedSearchLoader {
+ get: (id: string) => Promise;
+ urlFor: (id: string) => string;
+}
diff --git a/src/plugins/discover_legacy/public/url_generator.test.ts b/src/plugins/discover_legacy/public/url_generator.test.ts
new file mode 100644
index 000000000000..c352dd5133a4
--- /dev/null
+++ b/src/plugins/discover_legacy/public/url_generator.test.ts
@@ -0,0 +1,269 @@
+/*
+ * 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 { DiscoverUrlGenerator } from './url_generator';
+import { hashedItemStore, getStatesFromOsdUrl } from '../../opensearch_dashboards_utils/public';
+import { mockStorage } from '../../opensearch_dashboards_utils/public/storage/hashed_item_store/mock';
+import { FilterStateStore } from '../../data/common';
+
+const appBasePath: string = 'xyz/app/discover';
+const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002';
+const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d';
+
+interface SetupParams {
+ useHash?: boolean;
+}
+
+const setup = async ({ useHash = false }: SetupParams = {}) => {
+ const generator = new DiscoverUrlGenerator({
+ appBasePath,
+ useHash,
+ });
+
+ return {
+ generator,
+ };
+};
+
+beforeEach(() => {
+ // @ts-ignore
+ hashedItemStore.storage = mockStorage;
+});
+
+describe('Discover url generator', () => {
+ test('can create a link to Discover with no state and no saved search', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({});
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(url.startsWith(appBasePath)).toBe(true);
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can create a link to a saved search in Discover', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({ savedSearchId });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(url.startsWith(`${appBasePath}#/${savedSearchId}`)).toBe(true);
+ expect(_a).toEqual({});
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific index pattern', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ indexPatternId,
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ index: indexPatternId,
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify specific time range', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-15m',
+ mode: 'relative',
+ to: 'now',
+ },
+ });
+ });
+
+ test('can specify query', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ query: {
+ language: 'kuery',
+ query: 'foo',
+ },
+ });
+ expect(_g).toEqual({});
+ });
+
+ test('can specify local and global filters', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ filters: [
+ {
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.APP_STATE,
+ },
+ },
+ {
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ $state: {
+ store: FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ],
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'appState',
+ },
+ meta: {
+ alias: 'foo',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ expect(_g).toEqual({
+ filters: [
+ {
+ $state: {
+ store: 'globalState',
+ },
+ meta: {
+ alias: 'bar',
+ disabled: false,
+ negate: false,
+ },
+ },
+ ],
+ });
+ });
+
+ test('can set refresh interval', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ refreshInterval: {
+ pause: false,
+ value: 666,
+ },
+ });
+ });
+
+ test('can set time range', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ timeRange: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ const { _a, _g } = getStatesFromOsdUrl(url, ['_a', '_g']);
+
+ expect(_a).toEqual({});
+ expect(_g).toEqual({
+ time: {
+ from: 'now-3h',
+ to: 'now',
+ },
+ });
+ });
+
+ describe('useHash property', () => {
+ describe('when default useHash is set to false', () => {
+ test('when using default, sets index pattern ID in the generated URL', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ indexPatternId,
+ });
+
+ expect(url.indexOf(indexPatternId) > -1).toBe(true);
+ });
+
+ test('when enabling useHash, does not set index pattern ID in the generated URL', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ useHash: true,
+ indexPatternId,
+ });
+
+ expect(url.indexOf(indexPatternId) > -1).toBe(false);
+ });
+ });
+
+ describe('when default useHash is set to true', () => {
+ test('when using default, does not set index pattern ID in the generated URL', async () => {
+ const { generator } = await setup({ useHash: true });
+ const url = await generator.createUrl({
+ indexPatternId,
+ });
+
+ expect(url.indexOf(indexPatternId) > -1).toBe(false);
+ });
+
+ test('when disabling useHash, sets index pattern ID in the generated URL', async () => {
+ const { generator } = await setup();
+ const url = await generator.createUrl({
+ useHash: false,
+ indexPatternId,
+ });
+
+ expect(url.indexOf(indexPatternId) > -1).toBe(true);
+ });
+ });
+ });
+});
diff --git a/src/plugins/discover_legacy/public/url_generator.ts b/src/plugins/discover_legacy/public/url_generator.ts
new file mode 100644
index 000000000000..25e8517c8c9d
--- /dev/null
+++ b/src/plugins/discover_legacy/public/url_generator.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 {
+ TimeRange,
+ Filter,
+ Query,
+ opensearchFilters,
+ QueryState,
+ RefreshInterval,
+} from '../../data/public';
+import { setStateToOsdUrl } from '../../opensearch_dashboards_utils/public';
+import { UrlGeneratorsDefinition } from '../../share/public';
+
+export const DISCOVER_APP_URL_GENERATOR = 'DISCOVER_APP_URL_GENERATOR';
+
+export interface DiscoverUrlGeneratorState {
+ /**
+ * Optionally set saved search ID.
+ */
+ savedSearchId?: string;
+
+ /**
+ * Optionally set index pattern ID.
+ */
+ indexPatternId?: string;
+
+ /**
+ * Optionally set the time range in the time picker.
+ */
+ timeRange?: TimeRange;
+
+ /**
+ * Optionally set the refresh interval.
+ */
+ refreshInterval?: RefreshInterval;
+
+ /**
+ * Optionally apply filers.
+ */
+ filters?: Filter[];
+
+ /**
+ * Optionally set a query. NOTE: if given and used in conjunction with `dashboardId`, and the
+ * saved dashboard has a query saved with it, this will _replace_ that query.
+ */
+ query?: Query;
+
+ /**
+ * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
+ * whether to hash the data in the url to avoid url length issues.
+ */
+ useHash?: boolean;
+}
+
+interface Params {
+ appBasePath: string;
+ useHash: boolean;
+}
+
+export class DiscoverUrlGenerator
+ implements UrlGeneratorsDefinition {
+ constructor(private readonly params: Params) {}
+
+ public readonly id = DISCOVER_APP_URL_GENERATOR;
+
+ public readonly createUrl = async ({
+ filters,
+ indexPatternId,
+ query,
+ refreshInterval,
+ savedSearchId,
+ timeRange,
+ useHash = this.params.useHash,
+ }: DiscoverUrlGeneratorState): Promise => {
+ const savedSearchPath = savedSearchId ? encodeURIComponent(savedSearchId) : '';
+ const appState: {
+ query?: Query;
+ filters?: Filter[];
+ index?: string;
+ } = {};
+ const queryState: QueryState = {};
+
+ if (query) appState.query = query;
+ if (filters && filters.length)
+ appState.filters = filters?.filter((f) => !opensearchFilters.isFilterPinned(f));
+ if (indexPatternId) appState.index = indexPatternId;
+
+ if (timeRange) queryState.time = timeRange;
+ if (filters && filters.length)
+ queryState.filters = filters?.filter((f) => opensearchFilters.isFilterPinned(f));
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+
+ let url = `${this.params.appBasePath}#/${savedSearchPath}`;
+ url = setStateToOsdUrl('_g', queryState, { useHash }, url);
+ url = setStateToOsdUrl('_a', appState, { useHash }, url);
+
+ return url;
+ };
+}
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx
index 3865e9852882..f541456c93b4 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_data.tsx
@@ -45,6 +45,8 @@ export interface TopNavMenuData {
emphasize?: boolean;
iconType?: EuiIconType;
iconSide?: EuiButtonProps['iconSide'];
+ // @deprecated - experimental, do not use yet. Will be removed in a future minor version
+ type?: 'toggle' | 'button';
}
export interface RegisteredTopNavMenuData extends TopNavMenuData {
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx
index 7f987d937b96..bbc6ff1a7165 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx
@@ -30,7 +30,7 @@
import { upperFirst, isFunction } from 'lodash';
import React, { MouseEvent } from 'react';
-import { EuiToolTip, EuiButton, EuiHeaderLink } from '@elastic/eui';
+import { EuiToolTip, EuiButton, EuiHeaderLink, EuiSwitch } from '@elastic/eui';
import { TopNavMenuData } from './top_nav_menu_data';
export function TopNavMenuItem(props: TopNavMenuData) {
@@ -58,21 +58,36 @@ export function TopNavMenuItem(props: TopNavMenuData) {
className: props.className,
};
- const btn = props.emphasize ? (
-
- {upperFirst(props.label || props.id!)}
-
- ) : (
-
- {upperFirst(props.label || props.id!)}
-
- );
+ let component;
+ if (props.type === 'toggle') {
+ component = (
+ {
+ handleClick((e as unknown) as MouseEvent);
+ }}
+ data-test-subj={props.testId}
+ className={props.className}
+ />
+ );
+ } else {
+ component = props.emphasize ? (
+
+ {upperFirst(props.label || props.id!)}
+
+ ) : (
+
+ {upperFirst(props.label || props.id!)}
+
+ );
+ }
const tooltip = getTooltip();
if (tooltip) {
- return {btn};
+ return {component};
}
- return btn;
+ return component;
}
TopNavMenuItem.defaultProps = {
diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts
index 93a7fff7f2bc..7f9744119c73 100644
--- a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts
+++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts
@@ -43,11 +43,8 @@ describe('shortUrlAssertValid()', () => {
['hostname', 'localhost/app/opensearch-dashboards', PATH_ERROR], // according to spec, this is not a valid URL -- you cannot specify a hostname without a protocol
['hostname and port', 'local.host:5601/app/opensearch-dashboards', PROTOCOL_ERROR], // parser detects 'local.host' as the protocol
['hostname and auth', 'user:pass@localhost.net/app/opensearch-dashboards', PROTOCOL_ERROR], // parser detects 'user' as the protocol
- ['path traversal', '/app/../../not-opensearch-dashboards', PATH_ERROR], // fails because there are >2 path parts
['path traversal', '/../not-opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app'
- ['deep path', '/app/opensearch-dashboards/foo', PATH_ERROR], // fails because there are >2 path parts
- ['deeper path', '/app/opensearch-dashboards/foo/bar', PATH_ERROR], // fails because there are >2 path parts
- ['base path', '/base/app/opensearch-dashboards', PATH_ERROR], // fails because there are >2 path parts
+ ['base path', '/base/app/opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app'
['path with an extra leading slash', '//foo/app/opensearch-dashboards', HOSTNAME_ERROR], // parser detects 'foo' as the hostname
['path with an extra leading slash', '///app/opensearch-dashboards', HOSTNAME_ERROR], // parser detects '' as the hostname
['path without app', '/foo/opensearch-dashboards', PATH_ERROR], // fails because first path part is not 'app'
@@ -63,10 +60,13 @@ describe('shortUrlAssertValid()', () => {
const valid = [
'/app/opensearch-dashboards',
'/app/opensearch-dashboards/', // leading and trailing slashes are trimmed
+ '/app/opensearch-dashboards/deeper',
'/app/monitoring#angular/route',
'/app/text#document-id',
+ '/app/text/deeper#document-id',
'/app/some?with=query',
'/app/some?with=query#and-a-hash',
+ '/app/some/deeper?with=query#and-a-hash',
];
valid.forEach((url) => {
diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts
index aec6e743c0bc..b6514a68e6f6 100644
--- a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts
+++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts
@@ -48,7 +48,7 @@ export function shortUrlAssertValid(url: string) {
}
const pathnameParts = trim(pathname === null ? undefined : pathname, '/').split('/');
- if (pathnameParts.length !== 2 || pathnameParts[0] !== 'app' || !pathnameParts[1]) {
+ if (pathnameParts[0] !== 'app' || !pathnameParts[1]) {
throw Boom.notAcceptable(
`Short url target path must be in the format "/app/{{appId}}", found "${pathname}"`
);
diff --git a/src/test_utils/public/helpers/find_test_subject.ts b/src/test_utils/public/helpers/find_test_subject.ts
index 98687e3f0eef..ccb17b336059 100644
--- a/src/test_utils/public/helpers/find_test_subject.ts
+++ b/src/test_utils/public/helpers/find_test_subject.ts
@@ -54,8 +54,8 @@ const MATCHERS: Matcher[] = [
* @param testSubjectSelector The data test subject selector
* @param matcher optional matcher
*/
-export const findTestSubject = (
- reactWrapper: ReactWrapper,
+export const findTestSubject = (
+ reactWrapper: ReactWrapper,
testSubjectSelector: T,
matcher: Matcher = '~='
) => {
diff --git a/src/test_utils/public/testing_lib_helpers.tsx b/src/test_utils/public/testing_lib_helpers.tsx
new file mode 100644
index 000000000000..1e39a0cdcecc
--- /dev/null
+++ b/src/test_utils/public/testing_lib_helpers.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { ReactElement } from 'react';
+import { render as rtlRender } from '@testing-library/react';
+import { I18nProvider } from '@osd/i18n/react';
+
+// src: https://testing-library.com/docs/example-react-intl/#creating-a-custom-render-function
+function render(ui: ReactElement, { ...renderOptions } = {}) {
+ const Wrapper: React.FC = ({ children }) => {
+ return {children};
+ };
+ return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
+}
+
+// re-export everything
+export * from '@testing-library/react';
+
+// override render method
+export { render };
diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.js
index e612c8d3c41d..23350c81b18f 100644
--- a/test/functional/apps/context/_date_nanos.js
+++ b/test/functional/apps/context/_date_nanos.js
@@ -52,6 +52,7 @@ export default function ({ getService, getPageObjects }) {
await opensearchDashboardsServer.uiSettings.update({
'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
'context:step': `${TEST_STEP_SIZE}`,
+ 'discover:v2': false,
});
});
diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js
index 2c6bef3a366b..52864a0d7ea3 100644
--- a/test/functional/apps/context/_date_nanos_custom_timestamp.js
+++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js
@@ -52,6 +52,7 @@ export default function ({ getService, getPageObjects }) {
await opensearchDashboardsServer.uiSettings.update({
'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`,
'context:step': `${TEST_STEP_SIZE}`,
+ 'discover:v2': false,
});
});
diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js
index a20e6a6a40ca..17219a836230 100644
--- a/test/functional/apps/context/_filters.js
+++ b/test/functional/apps/context/_filters.js
@@ -38,11 +38,17 @@ export default function ({ getService, getPageObjects }) {
const docTable = getService('docTable');
const filterBar = getService('filterBar');
const retry = getService('retry');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
+ const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'context']);
describe('context filters', function contextSize() {
beforeEach(async function () {
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
+ await browser.refresh();
await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID, {
columns: TEST_COLUMN_NAMES,
});
diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js
index a5c3c94474ea..07fbfe00ac2b 100644
--- a/test/functional/apps/context/index.js
+++ b/test/functional/apps/context/index.js
@@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
await browser.setWindowSize(1200, 800);
await opensearchArchiver.loadIfNeeded('logstash_functional');
await opensearchArchiver.load('visualize');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'logstash-*',
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
});
diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js
index 5beca47f9cae..e23a2caba0c7 100644
--- a/test/functional/apps/dashboard/dashboard_state.js
+++ b/test/functional/apps/dashboard/dashboard_state.js
@@ -43,6 +43,7 @@ export default function ({ getService, getPageObjects }) {
'tileMap',
'visChart',
'timePicker',
+ 'common',
]);
const testSubjects = getService('testSubjects');
const browser = getService('browser');
@@ -52,11 +53,17 @@ export default function ({ getService, getPageObjects }) {
const retry = getService('retry');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardAddPanel = getService('dashboardAddPanel');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
describe('dashboard state', function describeIndexTests() {
before(async function () {
await PageObjects.dashboard.initTests();
await PageObjects.dashboard.preserveCrossAppState();
+
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
+ await browser.refresh();
});
after(async function () {
@@ -88,6 +95,8 @@ export default function ({ getService, getPageObjects }) {
expect(colorChoiceRetained).to.be(true);
});
+ // the following three tests are skipped because of save search save window bug:
+ // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4698
it('Saved search with no changes will update when the saved object changes', async () => {
await PageObjects.dashboard.gotoDashboardLandingPage();
@@ -107,6 +116,9 @@ export default function ({ getService, getPageObjects }) {
expect(inViewMode).to.be(true);
await PageObjects.header.clickDiscover();
+ // Add load save search here since discover link won't take it to the save search link for
+ // the legacy discover plugin
+ await PageObjects.discover.loadSavedSearch('my search');
await PageObjects.discover.clickFieldListItemAdd('agent');
await PageObjects.discover.saveSearch('my search');
await PageObjects.header.waitUntilLoadingHasFinished();
@@ -126,6 +138,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.saveDashboard('Has local edits');
await PageObjects.header.clickDiscover();
+ // Add load save search here since discover link won't take it to the save search link for
+ // the legacy discover plugin
+ await PageObjects.discover.loadSavedSearch('my search');
await PageObjects.discover.clickFieldListItemAdd('clientip');
await PageObjects.discover.saveSearch('my search');
await PageObjects.header.waitUntilLoadingHasFinished();
diff --git a/test/functional/apps/dashboard/dashboard_time_picker.js b/test/functional/apps/dashboard/dashboard_time_picker.js
index c1f4e50e6a65..5c97872cdb7e 100644
--- a/test/functional/apps/dashboard/dashboard_time_picker.js
+++ b/test/functional/apps/dashboard/dashboard_time_picker.js
@@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.clickNewDashboard();
log.debug('Clicked new dashboard');
await dashboardVisualizations.createAndAddSavedSearch({
- name: 'saved search',
+ name: 'saved search 1',
fields: ['bytes', 'agent'],
});
log.debug('added saved search');
diff --git a/test/functional/apps/dashboard/panel_context_menu.ts b/test/functional/apps/dashboard/panel_context_menu.ts
index 2d00c81581e6..07af447d4e15 100644
--- a/test/functional/apps/dashboard/panel_context_menu.ts
+++ b/test/functional/apps/dashboard/panel_context_menu.ts
@@ -38,12 +38,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardAddPanel = getService('dashboardAddPanel');
const dashboardVisualizations = getService('dashboardVisualizations');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
+ const listingTable = getService('listingTable');
const PageObjects = getPageObjects([
'dashboard',
'header',
'visualize',
'discover',
'timePicker',
+ 'common',
]);
const dashboardName = 'Dashboard Panel Controls Test';
@@ -114,6 +117,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const panelCount = await PageObjects.dashboard.getPanelCount();
expect(panelCount).to.be(0);
+ // need to find the correct save
+ await PageObjects.dashboard.saveDashboard(dashboardName);
});
});
@@ -121,11 +126,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const searchName = 'my search';
before(async () => {
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
+ await browser.refresh();
await PageObjects.header.clickDiscover();
await PageObjects.discover.clickNewSearchButton();
await dashboardVisualizations.createSavedSearch({ name: searchName, fields: ['bytes'] });
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.header.clickDashboard();
+ // Have to add steps to actually click on the dashboard; since added browser.refresh() will make
+ // clickDashboard() to only land on the dashboard listing page
+ // We need to add browser.refresh() so clickDiscover() lands correctly on the legacy discover page
+ await listingTable.clickItemLink('dashboard', dashboardName);
+ await PageObjects.header.waitUntilLoadingHasFinished();
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
if (inViewMode) await PageObjects.dashboard.switchToEditMode();
diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js
index 721a5de07871..e96d507087fe 100644
--- a/test/functional/apps/discover/_date_nanos.js
+++ b/test/functional/apps/discover/_date_nanos.js
@@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }) {
describe('date_nanos', function () {
before(async function () {
await opensearchArchiver.loadIfNeeded('date_nanos');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'date-nanos' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'date-nanos',
+ 'discover:v2': false,
+ });
await security.testUser.setRoles([
'opensearch_dashboards_admin',
'opensearch_dashboards_date_nanos',
diff --git a/test/functional/apps/discover/_date_nanos_mixed.js b/test/functional/apps/discover/_date_nanos_mixed.js
index 8578572dfbc5..05b94d3d1d67 100644
--- a/test/functional/apps/discover/_date_nanos_mixed.js
+++ b/test/functional/apps/discover/_date_nanos_mixed.js
@@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }) {
describe('date_nanos_mixed', function () {
before(async function () {
await opensearchArchiver.loadIfNeeded('date_nanos_mixed');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'timestamp-*' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'timestamp-*',
+ 'discover:v2': false,
+ });
await security.testUser.setRoles([
'opensearch_dashboards_admin',
'opensearch_dashboards_date_nanos_mixed',
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 66555ddd0851..d132454a090e 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -41,16 +41,17 @@ export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
};
describe('discover app', function describeIndexTests() {
before(async function () {
- // delete .kibana index and update configDoc
- await opensearchDashboardsServer.uiSettings.replace(defaultSettings);
-
log.debug('load opensearch-dashboards index with default index pattern');
await opensearchArchiver.load('discover');
+ // delete .kibana index and update configDoc
+ await opensearchDashboardsServer.uiSettings.replace(defaultSettings);
+
// and load a set of makelogs data
await opensearchArchiver.loadIfNeeded('logstash_functional');
log.debug('discover');
@@ -258,7 +259,10 @@ export default function ({ getService, getPageObjects }) {
});
});
it('should show bars in the correct time zone after switching', async function () {
- await opensearchDashboardsServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'dateFormat:tz': 'America/Phoenix',
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitOpenSearchDashboardsChrome();
await queryBar.clearQuery();
@@ -273,7 +277,10 @@ export default function ({ getService, getPageObjects }) {
});
describe('usage of discover:searchOnPageLoad', () => {
it('should fetch data from OpenSearch initially when discover:searchOnPageLoad is false', async function () {
- await opensearchDashboardsServer.uiSettings.replace({ 'discover:searchOnPageLoad': false });
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:searchOnPageLoad': false,
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitOpenSearchDashboardsChrome();
@@ -281,7 +288,10 @@ export default function ({ getService, getPageObjects }) {
});
it('should not fetch data from OpenSearch initially when discover:searchOnPageLoad is true', async function () {
- await opensearchDashboardsServer.uiSettings.replace({ 'discover:searchOnPageLoad': true });
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:searchOnPageLoad': true,
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitOpenSearchDashboardsChrome();
@@ -306,6 +316,7 @@ export default function ({ getService, getPageObjects }) {
it('should update the histogram timerange when the query is resubmitted', async function () {
await opensearchDashboardsServer.uiSettings.update({
'timepicker:timeDefaults': '{ "from": "2015-09-18T19:37:13.000Z", "to": "now"}',
+ 'discover:v2': false,
});
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.awaitOpenSearchDashboardsChrome();
diff --git a/test/functional/apps/discover/_discover_histogram.ts b/test/functional/apps/discover/_discover_histogram.ts
index 391fa97e00a9..f32a85add6fa 100644
--- a/test/functional/apps/discover/_discover_histogram.ts
+++ b/test/functional/apps/discover/_discover_histogram.ts
@@ -40,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const defaultSettings = {
defaultIndex: 'long-window-logstash-*',
'dateFormat:tz': 'Europe/Berlin',
+ 'discover:v2': false,
};
describe('discover histogram', function describeIndexTests() {
diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js
index bc0afcb92250..6978c363689b 100644
--- a/test/functional/apps/discover/_doc_navigation.js
+++ b/test/functional/apps/discover/_doc_navigation.js
@@ -37,6 +37,7 @@ export default function ({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']);
const opensearchArchiver = getService('opensearchArchiver');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
const retry = getService('retry');
describe('doc link in discover', function contextSize() {
@@ -45,6 +46,9 @@ export default function ({ getService, getPageObjects }) {
await opensearchArchiver.loadIfNeeded('discover');
await opensearchArchiver.loadIfNeeded('logstash_functional');
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.waitForDocTableLoadingComplete();
diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts
index ed7e30201cf2..166aa954c364 100644
--- a/test/functional/apps/discover/_doc_table.ts
+++ b/test/functional/apps/discover/_doc_table.ts
@@ -40,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
};
describe('discover doc table', function describeIndexTests() {
diff --git a/test/functional/apps/discover/_errors.ts b/test/functional/apps/discover/_errors.ts
index f2df3714e230..3251b9215e1a 100644
--- a/test/functional/apps/discover/_errors.ts
+++ b/test/functional/apps/discover/_errors.ts
@@ -33,6 +33,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const opensearchArchiver = getService('opensearchArchiver');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
const toasts = getService('toasts');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
@@ -40,6 +41,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async function () {
await opensearchArchiver.loadIfNeeded('logstash_functional');
await opensearchArchiver.load('invalid_scripted_field');
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
await PageObjects.common.navigateToApp('discover');
});
diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js
index a9c4ac34f735..90de964dc4ec 100644
--- a/test/functional/apps/discover/_field_data.js
+++ b/test/functional/apps/discover/_field_data.js
@@ -46,6 +46,7 @@ export default function ({ getService, getPageObjects }) {
await opensearchArchiver.load('discover');
await opensearchDashboardsServer.uiSettings.replace({
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
});
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts
index ecefc610b746..50ecb54d27ff 100644
--- a/test/functional/apps/discover/_field_visualize.ts
+++ b/test/functional/apps/discover/_field_visualize.ts
@@ -41,6 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker', 'visualize']);
const defaultSettings = {
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
};
describe('discover field visualize button', function () {
diff --git a/test/functional/apps/discover/_filter_editor.js b/test/functional/apps/discover/_filter_editor.js
index 482376994d79..7692d7e6148b 100644
--- a/test/functional/apps/discover/_filter_editor.js
+++ b/test/functional/apps/discover/_filter_editor.js
@@ -39,6 +39,7 @@ export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
};
describe('discover filter editor', function describeIndexTests() {
diff --git a/test/functional/apps/discover/_indexpattern_with_encoded_id.ts b/test/functional/apps/discover/_indexpattern_with_encoded_id.ts
index 4ff77a744c20..f2cb85fb9280 100644
--- a/test/functional/apps/discover/_indexpattern_with_encoded_id.ts
+++ b/test/functional/apps/discover/_indexpattern_with_encoded_id.ts
@@ -16,7 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('indexpattern with encoded id', () => {
before(async () => {
await opensearchArchiver.loadIfNeeded('index_pattern_with_encoded_id');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'with-encoded-id' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'with-encoded-id',
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
});
diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts
index 6aa50ec2b7df..1f89753c0336 100644
--- a/test/functional/apps/discover/_indexpattern_without_timefield.ts
+++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts
@@ -43,7 +43,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'opensearch_dashboards_timefield',
]);
await opensearchArchiver.loadIfNeeded('index_pattern_without_timefield');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'without-timefield' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'without-timefield',
+ 'discover:v2': false,
+ });
await PageObjects.common.navigateToApp('discover');
});
diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js
index 5c4262118fa0..5f4c2438fce1 100644
--- a/test/functional/apps/discover/_inspector.js
+++ b/test/functional/apps/discover/_inspector.js
@@ -53,6 +53,7 @@ export default function ({ getService, getPageObjects }) {
// delete .kibana index and update configDoc
await opensearchDashboardsServer.uiSettings.replace({
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
});
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/discover/_large_string.js b/test/functional/apps/discover/_large_string.js
index 6b0395aa02fd..2cad2b0daf90 100644
--- a/test/functional/apps/discover/_large_string.js
+++ b/test/functional/apps/discover/_large_string.js
@@ -47,7 +47,10 @@ export default function ({ getService, getPageObjects }) {
]);
await opensearchArchiver.load('empty_opensearch_dashboards');
await opensearchArchiver.loadIfNeeded('hamlet');
- await opensearchDashboardsServer.uiSettings.replace({ defaultIndex: 'testlargestring' });
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'testlargestring',
+ 'discover:v2': false,
+ });
});
it('verify the large string book present', async function () {
diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js
index 6ba2002653af..c51850eac00d 100644
--- a/test/functional/apps/discover/_saved_queries.js
+++ b/test/functional/apps/discover/_saved_queries.js
@@ -35,11 +35,12 @@ export default function ({ getService, getPageObjects }) {
const log = getService('log');
const opensearchArchiver = getService('opensearchArchiver');
const opensearchDashboardsServer = getService('opensearchDashboardsServer');
- const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
const browser = getService('browser');
const defaultSettings = {
defaultIndex: 'logstash-*',
+ 'discover:v2': false,
};
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
@@ -54,6 +55,7 @@ export default function ({ getService, getPageObjects }) {
// and load a set of makelogs data
await opensearchArchiver.loadIfNeeded('logstash_functional');
await opensearchDashboardsServer.uiSettings.replace(defaultSettings);
+
log.debug('discover');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js
index 96b7f705c73a..d18481011560 100644
--- a/test/functional/apps/discover/_shared_links.js
+++ b/test/functional/apps/discover/_shared_links.js
@@ -62,6 +62,7 @@ export default function ({ getService, getPageObjects }) {
await opensearchDashboardsServer.uiSettings.replace({
'state:storeInSessionStorage': storeStateInSessionStorage,
+ 'discover:v2': false,
});
log.debug('discover');
@@ -96,7 +97,7 @@ export default function ({ getService, getPageObjects }) {
it('should allow for copying the snapshot URL', async function () {
const expectedUrl =
baseUrl +
- '/app/discover?_t=1453775307251#' +
+ '/app/discoverLegacy?_t=1453775307251#' +
'/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' +
":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" +
"-23T18:31:44.000Z'))&_a=(columns:!(_source),filters:!(),index:'logstash-" +
@@ -121,7 +122,7 @@ export default function ({ getService, getPageObjects }) {
it('should allow for copying the saved object URL', async function () {
const expectedUrl =
baseUrl +
- '/app/discover#' +
+ '/app/discoverLegacy#' +
'/view/ab12e3c0-f231-11e6-9486-733b1ac9221a' +
'?_g=(filters%3A!()%2CrefreshInterval%3A(pause%3A!t%2Cvalue%3A0)' +
"%2Ctime%3A(from%3A'2015-09-19T06%3A31%3A44.000Z'%2C" +
@@ -160,7 +161,7 @@ export default function ({ getService, getPageObjects }) {
await browser.get(actualUrl, false);
await retry.waitFor('shortUrl resolves and opens', async () => {
const resolvedUrl = await browser.getCurrentUrl();
- expect(resolvedUrl).to.match(/discover/);
+ expect(resolvedUrl).to.match(/discoverLegacy/);
const resolvedTime = await PageObjects.timePicker.getTimeConfig();
expect(resolvedTime.start).to.equal(actualTime.start);
expect(resolvedTime.end).to.equal(actualTime.end);
@@ -175,7 +176,7 @@ export default function ({ getService, getPageObjects }) {
await browser.get(currentUrl, false);
await retry.waitFor('discover to open', async () => {
const resolvedUrl = await browser.getCurrentUrl();
- expect(resolvedUrl).to.match(/discover/);
+ expect(resolvedUrl).to.match(/discoverLegacy/);
const { message } = await toasts.getErrorToast();
expect(message).to.contain(
'Unable to completely restore the URL, be sure to use the share functionality.'
diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js
index 446cb1b760e0..5d6bcb5134e6 100644
--- a/test/functional/apps/discover/_sidebar.js
+++ b/test/functional/apps/discover/_sidebar.js
@@ -38,17 +38,18 @@ export default function ({ getService, getPageObjects }) {
describe('discover sidebar', function describeIndexTests() {
before(async function () {
- // delete .kibana index and update configDoc
- await opensearchDashboardsServer.uiSettings.replace({
- defaultIndex: 'logstash-*',
- });
-
log.debug('load opensearch-dashboards index with default index pattern');
await opensearchArchiver.load('discover');
// and load a set of makelogs data
await opensearchArchiver.loadIfNeeded('logstash_functional');
+ // delete .kibana index and update configDoc
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'logstash-*',
+ 'discover:v2': false,
+ });
+
log.debug('discover');
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/discover/_source_filters.js b/test/functional/apps/discover/_source_filters.js
index d324a0972b8a..1f425a4e341f 100644
--- a/test/functional/apps/discover/_source_filters.js
+++ b/test/functional/apps/discover/_source_filters.js
@@ -38,17 +38,18 @@ export default function ({ getService, getPageObjects }) {
describe('source filters', function describeIndexTests() {
before(async function () {
- // delete .kibana index and update configDoc
- await opensearchDashboardsServer.uiSettings.replace({
- defaultIndex: 'logstash-*',
- });
-
log.debug('load opensearch-dashboards index with default index pattern');
await opensearchArchiver.load('visualize_source-filters');
// and load a set of makelogs data
await opensearchArchiver.loadIfNeeded('logstash_functional');
+ // delete .kibana index and update configDoc
+ await opensearchDashboardsServer.uiSettings.replace({
+ defaultIndex: 'logstash-*',
+ 'discover:v2': false,
+ });
+
log.debug('discover');
await PageObjects.common.navigateToApp('discover');
diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts
index 03230f1270ed..36f8e50ea543 100644
--- a/test/functional/apps/home/_navigation.ts
+++ b/test/functional/apps/home/_navigation.ts
@@ -36,11 +36,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']);
const appsMenu = getService('appsMenu');
const opensearchArchiver = getService('opensearchArchiver');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
describe('OpenSearch Dashboards browser back navigation should work', function describeIndexTests() {
before(async () => {
await opensearchArchiver.loadIfNeeded('discover');
await opensearchArchiver.loadIfNeeded('logstash_functional');
+
+ await opensearchDashboardsServer.uiSettings.replace({ 'discover:v2': false });
});
it('detect navigate back issues', async () => {
diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js
index c5c6456f0442..203345e38f91 100644
--- a/test/functional/apps/management/_handle_alias.js
+++ b/test/functional/apps/management/_handle_alias.js
@@ -32,6 +32,7 @@ import expect from '@osd/expect';
export default function ({ getService, getPageObjects }) {
const opensearchArchiver = getService('opensearchArchiver');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
const opensearch = getService('legacyOpenSearch');
const retry = getService('retry');
const security = getService('security');
@@ -42,6 +43,9 @@ export default function ({ getService, getPageObjects }) {
await security.testUser.setRoles(['opensearch_dashboards_admin', 'test_alias_reader']);
await opensearchArchiver.loadIfNeeded('alias');
await opensearchArchiver.load('empty_opensearch_dashboards');
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
await opensearch.indices.updateAliases({
body: {
actions: [
diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js
index 51f0dc44d771..0a6df08a998e 100644
--- a/test/functional/apps/management/_scripted_fields.js
+++ b/test/functional/apps/management/_scripted_fields.js
@@ -71,8 +71,9 @@ export default function ({ getService, getPageObjects }) {
before(async function () {
await browser.setWindowSize(1200, 800);
await opensearchArchiver.load('discover');
- // delete .kibana index and then wait for OpenSearch Dashboards to re-create it
- await opensearchDashboardsServer.uiSettings.replace({});
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
await opensearchDashboardsServer.uiSettings.update({});
});
diff --git a/test/functional/apps/visualize/_lab_mode.js b/test/functional/apps/visualize/_lab_mode.js
index 82ecbcb2a655..d852ac484eaa 100644
--- a/test/functional/apps/visualize/_lab_mode.js
+++ b/test/functional/apps/visualize/_lab_mode.js
@@ -34,8 +34,14 @@ import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualiza
export default function ({ getService, getPageObjects }) {
const log = getService('log');
const PageObjects = getPageObjects(['common', 'header', 'discover', 'settings']);
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
describe('visualize lab mode', () => {
+ before(async () => {
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
+ });
it('disabling does not break loading saved searches', async () => {
await PageObjects.common.navigateToUrl('discover', '', { useActualUrl: true });
await PageObjects.discover.saveSearch('visualize_lab_mode_test');
diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts
index fb7e721db7de..2bdc5990b928 100644
--- a/test/functional/apps/visualize/index.ts
+++ b/test/functional/apps/visualize/index.ts
@@ -49,6 +49,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await opensearchDashboardsServer.uiSettings.replace({
defaultIndex: 'logstash-*',
[UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b',
+ 'discover:v2': false,
});
isOss = await deployment.isOss();
});
diff --git a/test/functional/config.js b/test/functional/config.js
index d927aea2966f..b862208276bf 100644
--- a/test/functional/config.js
+++ b/test/functional/config.js
@@ -80,7 +80,7 @@ export default async function ({ readConfigFile }) {
pathname: '/status',
},
discover: {
- pathname: '/app/discover',
+ pathname: '/app/discoverLegacy',
hash: '/',
},
context: {
diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts
index aee71c8d58ba..588368d4b4a1 100644
--- a/test/functional/services/dashboard/visualizations.ts
+++ b/test/functional/services/dashboard/visualizations.ts
@@ -37,6 +37,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F
const queryBar = getService('queryBar');
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
+ const browser = getService('browser');
+ const opensearchDashboardsServer = getService('opensearchDashboardsServer');
const PageObjects = getPageObjects([
'dashboard',
'visualize',
@@ -44,6 +46,8 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F
'header',
'discover',
'timePicker',
+ 'common',
+ 'settings',
]);
return new (class DashboardVisualizations {
@@ -69,6 +73,11 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F
fields?: string[];
}) {
log.debug(`createSavedSearch(${name})`);
+
+ await opensearchDashboardsServer.uiSettings.replace({
+ 'discover:v2': false,
+ });
+ await browser.refresh();
await PageObjects.header.clickDiscover();
await PageObjects.timePicker.setHistoricalDataRange();