diff --git a/.i18nrc.json b/.i18nrc.json index 71c7970affa..234aa8de0c2 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -22,6 +22,7 @@ "interpreter": "src/legacy/core_plugins/interpreter", "osd": "src/legacy/core_plugins/opensearch-dashboards", "osdDocViews": "src/legacy/core_plugins/osd_doc_views", + "osdDocViewsLinks": "src/legacy/core_plugins/osd_doc_views_links", "management": [ "src/legacy/core_plugins/management", "src/plugins/management" diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index 5ece8659260..1b0e03653d0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -41,6 +41,7 @@ import { setScopedHistory, setServices, setDocViewsRegistry, + setDocViewsLinksRegistry, } from '../../../../opensearch_dashboards_services'; import { coreMock } from '../../../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../../../data/public/mocks'; @@ -98,6 +99,18 @@ describe('Doc Table', () => { }, }); + setDocViewsLinksRegistry({ + addDocViewLink(view) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }); + getInnerAngularModule( 'app/discover', core, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html index fd20bea8fb3..a28f1b9906c 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html @@ -15,28 +15,12 @@ -
-
-
- -
-
- -
-
+
+
diff --git a/src/plugins/discover/public/application/angular/doc_viewer_links.tsx b/src/plugins/discover/public/application/angular/doc_viewer_links.tsx new file mode 100644 index 00000000000..763a75e5130 --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_viewer_links.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DocViewerLinks } from '../components/doc_viewer_links/doc_viewer_links'; + +export function createDocViewerLinksDirective(reactDirective: any) { + return reactDirective( + (props: any) => { + return ; + }, + [ + 'hit', + ['indexPattern', { watchDepth: 'reference' }], + ['columns', { watchDepth: 'collection' }], + ], + { + restrict: 'E', + scope: { + hit: '=', + indexPattern: '=', + columns: '=?', + }, + } + ); +} diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index b107ae46b37..7385f0d360a 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -63,6 +63,17 @@ jest.mock('../../../opensearch_dashboards_services', () => { registry = []; }, }), + getDocViewsLinksRegistry: () => ({ + addDocViewLink(view: any) { + registry.push(view); + }, + getDocViewsLinksSorted() { + return registry; + }, + resetRegistry: () => { + registry = []; + }, + }), }; }); diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap new file mode 100644 index 00000000000..95fb0c37718 --- /dev/null +++ b/src/plugins/discover/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/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.test.tsx new file mode 100644 index 00000000000..8aba555b3a3 --- /dev/null +++ b/src/plugins/discover/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/public/application/components/doc_viewer_links/doc_viewer_links.tsx b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx new file mode 100644 index 00000000000..9efb0693fde --- /dev/null +++ b/src/plugins/discover/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/public/application/doc_views_links/doc_views_links_registry.ts b/src/plugins/discover/public/application/doc_views_links/doc_views_links_registry.ts new file mode 100644 index 00000000000..16653f5d537 --- /dev/null +++ b/src/plugins/discover/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/public/application/doc_views_links/doc_views_links_types.ts b/src/plugins/discover/public/application/doc_views_links/doc_views_links_types.ts new file mode 100644 index 00000000000..bbc5caadafc --- /dev/null +++ b/src/plugins/discover/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/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 6764f6cacce..b4a7a17357a 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -51,6 +51,7 @@ import { createTableRowDirective } from './application/angular/doc_table/compone import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './application/angular/doc_viewer'; +import { createDocViewerLinksDirective } from './application/angular/doc_viewer_links'; import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; import { initAngularBootstrap, @@ -202,5 +203,6 @@ function createDocTableModule() { .directive('osdTableRow', createTableRowDirective) .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) .directive('osdInfiniteScroll', createInfiniteScrollDirective) - .directive('docViewer', createDocViewerDirective); + .directive('docViewer', createDocViewerDirective) + .directive('docViewerLinks', createDocViewerLinksDirective); } diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 6f4d7b8ea4f..4724ced290f 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -38,6 +38,9 @@ const createSetupContract = (): Setup => { docViews: { addDocView: jest.fn(), }, + docViewsLinks: { + addDocViewLink: jest.fn(), + }, }; return setupContract; }; diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts index eb06e88ecac..8531564e0cc 100644 --- a/src/plugins/discover/public/opensearch_dashboards_services.ts +++ b/src/plugins/discover/public/opensearch_dashboards_services.ts @@ -36,6 +36,7 @@ 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; @@ -81,6 +82,10 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( 'DocViewsRegistry' ); + +export const [getDocViewsLinksRegistry, setDocViewsLinksRegistry] = createGetterSetter< + DocViewsLinksRegistry +>('DocViewsLinksRegistry'); /** * Makes sure discover and context are using one instance of history. */ diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index ea8659489d4..62f6e6908ba 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -54,17 +54,22 @@ import { 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 { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; -import { createOsdUrlTracker } from '../../opensearch_dashboards_utils/public'; +import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/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, @@ -103,6 +108,10 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + docViewsLinks: { + addDocViewLink(docViewLinkRaw: DocViewLink): void; + }; } export interface DiscoverStart { @@ -171,6 +180,7 @@ export class DiscoverPlugin 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; @@ -205,6 +215,7 @@ export class DiscoverPlugin order: 10, component: DocViewTable, }); + this.docViewsRegistry.addDocView({ title: i18n.translate('discover.docViews.json.jsonTitle', { defaultMessage: 'JSON', @@ -213,6 +224,52 @@ export class DiscoverPlugin 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, @@ -319,6 +376,9 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + docViewsLinks: { + addDocViewLink: this.docViewsLinksRegistry.addDocViewLink.bind(this.docViewsLinksRegistry), + }, }; } diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index e2f725c50bc..e733a4e3636 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -49,6 +49,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./test_suites/core_plugins'), require.resolve('./test_suites/management'), require.resolve('./test_suites/doc_views'), + require.resolve('./test_suites/doc_views_links'), require.resolve('./test_suites/application_links'), require.resolve('./test_suites/data_plugin'), ], diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json b/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json new file mode 100644 index 00000000000..0cda322f616 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/opensearch_dashboards.json @@ -0,0 +1,8 @@ +{ + "id": "docViewLinksPlugin", + "version": "0.0.1", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": false, + "ui": true, + "requiredPlugins": ["discover"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/package.json b/test/plugin_functional/plugins/doc_views_links_plugin/package.json new file mode 100644 index 00000000000..92cfca07815 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "docViewLinksPlugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/doc_views_links_plugin", + "opensearchDashboards": { + "version": "opensearchDashboards", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "osd": "node ../../../../scripts/osd.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts b/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts new file mode 100644 index 00000000000..047330d37b3 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/public/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DocViewsLinksPlugin } from './plugin'; + +export const plugin = () => new DocViewsLinksPlugin(); diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx b/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx new file mode 100644 index 00000000000..174e46a529a --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/public/plugin.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Plugin, CoreSetup } from 'opensearch-dashboards/public'; +import { DiscoverSetup } from '../../../../../src/plugins/discover/public'; + +export class DocViewsLinksPlugin implements Plugin { + public setup(core: CoreSetup, { discover }: { discover: DiscoverSetup }) { + discover.docViewsLinks.addDocViewLink({ + href: 'http://some-url/', + order: 1, + label: 'href doc view link', + }); + + discover.docViewsLinks.addDocViewLink({ + generateCb: () => ({ + url: 'http://some-url/', + }), + order: 2, + label: 'generateCb doc view link', + }); + + discover.docViewsLinks.addDocViewLink({ + generateCb: () => ({ + url: 'http://some-url/', + hide: true, + }), + order: 3, + label: 'generateCbHidden doc view link', + }); + } + + public start() {} +} diff --git a/test/plugin_functional/plugins/doc_views_links_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_links_plugin/tsconfig.json new file mode 100644 index 00000000000..8a481ba6e64 --- /dev/null +++ b/test/plugin_functional/plugins/doc_views_links_plugin/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*" + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} \ No newline at end of file diff --git a/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts b/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts new file mode 100644 index 00000000000..2933e0add11 --- /dev/null +++ b/test/plugin_functional/test_suites/doc_views_links/doc_views_links.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + + describe('custom doc views links', function () { + beforeEach(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await testSubjects.click('docTableExpandToggleColumn'); + }); + + it('should show href and generateCb doc views link', async () => { + const hrefLink = await find.byLinkText('href doc view link'); + const generateCbLink = await find.byLinkText('generateCb doc view link'); + + expect(await hrefLink.isDisplayed()).to.be(true); + expect(await generateCbLink.isDisplayed()).to.be(true); + }); + + it('should not render generateCbHidden doc views link', async () => { + expect(await find.existsByLinkText('generateCbHidden doc view link')).to.eql(false); + }); + + it('should render href doc view link', async () => { + const hrefLink = await find.byLinkText('href doc view link'); + await hrefLink.click(); + expect(await browser.getCurrentUrl()).to.eql('http://some-url/'); + }); + + it('should render generateCb doc view link', async () => { + const generateCbLink = await find.byLinkText('generateCb doc view link'); + await generateCbLink.click(); + expect(await browser.getCurrentUrl()).to.eql('http://some-url/'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/doc_views_links/index.ts b/test/plugin_functional/test_suites/doc_views_links/index.ts new file mode 100644 index 00000000000..b894edb4db0 --- /dev/null +++ b/test/plugin_functional/test_suites/doc_views_links/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, loadTestFile }: PluginFunctionalProviderContext) { + const opensearchArchiver = getService('opensearchArchiver'); + + describe('doc views links', function () { + before(async () => { + await opensearchArchiver.loadIfNeeded('../functional/fixtures/opensearch_archiver/discover'); + }); + + loadTestFile(require.resolve('./doc_views_links')); + }); +}