@@ -653,6 +695,12 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
"render": [Function],
"sortable": true,
},
+ Object {
+ "dataType": "string",
+ "field": "type",
+ "name": "Type",
+ "sortable": true,
+ },
Object {
"dataType": "string",
"field": "description",
@@ -689,6 +737,7 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `
},
}
}
+ viewItem={[Function]}
/>
`;
diff --git a/src/plugins/dashboard/public/application/listing/create_button.test.tsx b/src/plugins/dashboard/public/application/listing/create_button.test.tsx
new file mode 100644
index 00000000000..5d2a200f55d
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/create_button.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+import React from 'react';
+
+import { CreateButton } from './create_button';
+import { DashboardProvider } from '../../types';
+
+const provider = (type?: string, url?: string, text?: string): DashboardProvider => {
+ return {
+ appId: 'test',
+ savedObjectsType: type || 'test',
+ savedObjectsName: type || 'Test',
+ createUrl: url || 'createUrl',
+ createLinkText: text || 'TestModule',
+ createSortText: text || 'TestModule',
+ viewUrlPathFn: (id) => `/${type || 'test'}_plugin/${id}`,
+ editUrlPathFn: (id) => `/${type || 'test'}_plugin/${id}/edit`,
+ };
+};
+
+function mountComponent(props?: any) {
+ return mountWithIntl(
);
+}
+
+describe('create button no props', () => {
+ test('renders empty when no providers given', () => {
+ const component = mountComponent();
+
+ expect(component).toMatchSnapshot();
+ });
+});
+describe('create button with props', () => {
+ test('renders single button when one provider given', () => {
+ const component = mountComponent({ dashboardProviders: [provider()] });
+ expect(component).toMatchSnapshot();
+ const links = findTestSubject(component, 'newItemButton');
+ expect(links.length).toBe(1);
+ });
+ test('renders button dropdown menu when two providers given', () => {
+ const provider1 = provider('test1', 'test1', 'test1');
+ const provider2 = provider('test2', 'test2', 'test2');
+ const component = mountComponent({ dashboardProviders: [provider2, provider1] });
+ expect(component).toMatchSnapshot();
+ const createButtons = findTestSubject(component, 'newItemButton');
+ expect(createButtons.length).toBe(0);
+ const createDropdown = findTestSubject(component, 'createMenuDropdown');
+ createDropdown.simulate('click');
+ const contextMenus = findTestSubject(component, 'contextMenuItem');
+ expect(contextMenus.length).toBe(2);
+ expect(contextMenus.at(0).prop('href')).toBe('test1');
+ });
+});
diff --git a/src/plugins/dashboard/public/application/listing/create_button.tsx b/src/plugins/dashboard/public/application/listing/create_button.tsx
new file mode 100644
index 00000000000..04e6df88377
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/create_button.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import React, { useState } from 'react';
+import { FormattedMessage } from '@osd/i18n/react';
+import {
+ EuiButton,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFlexItem,
+ EuiPopover,
+} from '@elastic/eui';
+import type { DashboardProvider } from '../../types';
+
+interface CreateButtonProps {
+ dashboardProviders?: DashboardProvider[];
+}
+
+const CreateButton = (props: CreateButtonProps) => {
+ const [isPopoverOpen, setPopover] = useState(false);
+
+ const onMenuButtonClick = () => {
+ setPopover(!isPopoverOpen);
+ };
+
+ const closePopover = () => {
+ setPopover(false);
+ };
+
+ const getPopupMenuItems = () => {
+ const providers = Object.values(props.dashboardProviders || {});
+ return providers
+ .sort((a: DashboardProvider, b: DashboardProvider) =>
+ a.createSortText.localeCompare(b.createSortText)
+ )
+ .map((provider: DashboardProvider) => (
+
+ {provider.createLinkText}
+
+ ));
+ };
+
+ const renderCreateMenuDropDown = () => {
+ const button = (
+
+
+
+ );
+
+ return (
+
+
+
+ );
+ };
+
+ const renderCreateSingleButton = () => {
+ const provider: DashboardProvider = Object.values(props.dashboardProviders!)[0];
+ return (
+
+
+
+ {provider.createLinkText}
+
+
+ );
+ };
+
+ const renderMenu = () => {
+ if (!props.dashboardProviders || Object.keys(props.dashboardProviders!).length === 0) {
+ return null;
+ } else if (Object.keys(props.dashboardProviders!).length === 1) {
+ return renderCreateSingleButton();
+ } else {
+ return renderCreateMenuDropDown();
+ }
+ };
+
+ return renderMenu();
+};
+
+export { CreateButton };
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.js
index 1864c2852ae..7e43bc96faf 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_listing.js
+++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.js
@@ -37,6 +37,7 @@ import { i18n } from '@osd/i18n';
import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { TableListView } from '../../../../opensearch_dashboards_react/public';
+import { CreateButton } from './create_button';
export const EMPTY_FILTER = '';
@@ -56,9 +57,15 @@ export class DashboardListing extends React.Component {
+ )
+ }
findItems={this.props.findItems}
deleteItems={this.props.hideWriteControls ? null : this.props.deleteItems}
editItem={this.props.hideWriteControls ? null : this.props.editItem}
+ viewItem={this.props.hideWriteControls ? null : this.props.viewItem}
tableColumns={this.getTableColumns()}
listingLimit={this.props.listingLimit}
initialFilter={this.props.initialFilter}
@@ -163,7 +170,8 @@ export class DashboardListing extends React.Component {
getTableColumns() {
const dateFormat = this.props.core.uiSettings.get('dateFormat');
- const tableColumns = [
+
+ return [
{
field: 'title',
name: i18n.translate('dashboard.listing.table.titleColumnName', {
@@ -172,13 +180,21 @@ export class DashboardListing extends React.Component {
sortable: true,
render: (field, record) => (
{field}
),
},
+ {
+ field: 'type',
+ name: i18n.translate('dashboard.listing.table.typeColumnName', {
+ defaultMessage: 'Type',
+ }),
+ dataType: 'string',
+ sortable: true,
+ },
{
field: 'description',
name: i18n.translate('dashboard.listing.table.descriptionColumnName', {
@@ -201,16 +217,18 @@ export class DashboardListing extends React.Component {
render: (updatedAt) => updatedAt && moment(updatedAt).format(dateFormat),
},
];
- return tableColumns;
}
}
DashboardListing.propTypes = {
- createItem: PropTypes.func.isRequired,
+ createItem: PropTypes.func,
+ dashboardProviders: PropTypes.object,
findItems: PropTypes.func.isRequired,
deleteItems: PropTypes.func.isRequired,
editItem: PropTypes.func.isRequired,
- getViewUrl: PropTypes.func.isRequired,
+ getViewUrl: PropTypes.func,
+ editItemAvailable: PropTypes.func,
+ viewItem: PropTypes.func,
listingLimit: PropTypes.number.isRequired,
hideWriteControls: PropTypes.bool.isRequired,
initialFilter: PropTypes.string,
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js
index bb469003da0..7bce8de4208 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js
+++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.js
@@ -70,7 +70,10 @@ test('renders empty page in before initial fetch to avoid flickering', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
+ initialPageSize={10}
listingLimit={1000}
hideWriteControls={false}
core={{ notifications: { toasts: {} }, uiSettings: { get: jest.fn(() => 10) } }}
@@ -87,7 +90,9 @@ describe('after fetch', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
listingLimit={1000}
hideWriteControls={false}
initialPageSize={10}
@@ -111,7 +116,9 @@ describe('after fetch', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
listingLimit={1000}
initialPageSize={10}
hideWriteControls={false}
@@ -134,7 +141,9 @@ describe('after fetch', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
listingLimit={1}
initialPageSize={10}
hideWriteControls={false}
@@ -157,7 +166,9 @@ describe('after fetch', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
listingLimit={1}
initialPageSize={10}
hideWriteControls={true}
@@ -180,7 +191,9 @@ describe('after fetch', () => {
deleteItems={() => {}}
createItem={() => {}}
editItem={() => {}}
- getViewUrl={() => {}}
+ viewItem={() => {}}
+ dashboardItemCreatorClickHandler={() => {}}
+ dashboardItemCreators={() => []}
listingLimit={1}
initialPageSize={10}
hideWriteControls={false}
diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html
index ba05c138a0c..5e19fbfe678 100644
--- a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html
+++ b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html
@@ -1,8 +1,10 @@
void;
+
+export interface DashboardSetup {
+ registerDashboardProvider: RegisterDashboardProviderFn;
+}
export interface DashboardStart {
getSavedDashboardLoader: () => SavedObjectLoader;
@@ -200,6 +207,7 @@ export class DashboardPlugin
private currentHistory: ScopedHistory | undefined = undefined;
private dashboardFeatureFlagConfig?: DashboardFeatureFlagConfig;
+ private dashboardProviders: { [key: string]: DashboardProvider } = {};
private dashboardUrlGenerator?: DashboardUrlGenerator;
public setup(
@@ -308,6 +316,48 @@ export class DashboardPlugin
stopUrlTracker();
};
+ const registerDashboardProvider: RegisterDashboardProviderFn = (
+ provider: DashboardProvider
+ ) => {
+ const found = this.dashboardProviders[provider.savedObjectsType];
+ if (found) {
+ throw new Error(`DashboardProvider ${provider.savedObjectsType} is registered twice`);
+ }
+ if (
+ isEmpty(provider.createSortText) ||
+ isEmpty(provider.createUrl) ||
+ isEmpty(provider.createLinkText)
+ ) {
+ throw new Error(
+ `DashboardProvider ${provider.savedObjectsType} requires 'createSortText', 'createLinkText', and 'createUrl'`
+ );
+ }
+ if (isEmpty(provider.savedObjectsType || isEmpty(provider.savedObjectsName))) {
+ throw new Error(
+ `DashboardProvider ${provider.savedObjectsType} requires 'savedObjectsId', and 'savedObjectsType'`
+ );
+ }
+
+ this.dashboardProviders[provider.savedObjectsType] = provider;
+ };
+
+ registerDashboardProvider({
+ savedObjectsType: 'dashboard',
+ savedObjectsName: 'Dashboard',
+ appId: 'dashboards',
+ viewUrlPathFn: (obj) => `#/view/${obj.id}`,
+ editUrlPathFn: (obj) => `/view/${obj.id}?_a=(viewMode:edit)`,
+ createUrl: core.http.basePath.prepend('/app/dashboards#/create'),
+ createSortText: 'Dashboard',
+ createLinkText: (
+
+ ),
+ });
+
const app: App = {
id: DashboardConstants.DASHBOARDS_ID,
title: 'Dashboard',
@@ -341,6 +391,7 @@ export class DashboardPlugin
data: dataStart,
savedObjectsClient: coreStart.savedObjects.client,
savedDashboards: dashboardStart.getSavedDashboardLoader(),
+ dashboardProviders: () => this.dashboardProviders,
chrome: coreStart.chrome,
addBasePath: coreStart.http.basePath.prepend,
uiSettings: coreStart.uiSettings,
@@ -420,6 +471,10 @@ export class DashboardPlugin
order: 100,
});
}
+
+ return {
+ registerDashboardProvider,
+ };
}
private addEmbeddableToDashboard(
diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts
index 49705c8614d..42a07b40ef1 100644
--- a/src/plugins/dashboard/public/types.ts
+++ b/src/plugins/dashboard/public/types.ts
@@ -141,3 +141,74 @@ export interface StagedFilter {
operator: string;
index: string;
}
+
+export interface DashboardProvider {
+ // appId :
+ // The appId used to register this Plugin application.
+ // This value needs to be repeated here as the 'app' of this plugin
+ // is not directly referenced in the details below, and the 'app' object
+ // is not linked in the Dashboards List surrounding code.
+ appId: string;
+
+ // savedObjectstype :
+ // This string should be the SavedObjects 'type' that you
+ // have registered for your objects. This must match the value
+ // used by your Plugin's Server setup with `savedObjects.registerType()` call.
+ savedObjectsType: string;
+
+ // savedObjectsName :
+ // This string should be the display-name that will be used on the
+ // Dashboads / Dashboards table in a column named "Type".
+ savedObjectsName: string;
+
+ // savedObjectsId : Optional
+ // If provided, this string will override the use of the `savedObjectsType`
+ // for use with querying the SavedObjects index for your objects.
+ // The default value for this string is implicitly set to the `savedObjectsType`
+ savedObjectsId?: string;
+
+ // createLinkText :
+ // this is the string or Element that will be used to construct the
+ // OUI MenuPopup of Create options.
+ createLinkText: string | JSX.Element;
+
+ // createSortText :
+ // This string will be used in sorting the Create options. Use
+ // the verbatim string here, not any interpolation or function.
+ createSortText: string;
+
+ // createUrl :
+ // This string should be the url-path for your plugin's Create
+ // feature.
+ createUrl: string;
+
+ // viewUrlPathFn :
+ // This function will be called on every iteratee of your objects
+ // while querying the SavedObjects for Dashboards / Dashboards
+ // This function should return the url-path to the View page
+ // for your Plugin's objects, within the "app" basepath.
+ // For instance :
+ // appId = "myplugin"
+ // app.basepath is then "/app/myplugin"
+ // then
+ // viewUrlPathFn: (obj) => `#/view/${obj.id}`
+ //
+ // At onClick of rendered table "view" link for item {id: 'abc123', ...}, the navigated path will be:
+ // "http://../app/myplugin#/view/abc123"
+ viewUrlPathFn: (obj: SavedObjectType) => string;
+
+ // editUrlPathFn :
+ // This function will be called on every iteratee of your objects
+ // while querying the SavedObjects for Dashboards / Dashboards
+ // This function should return the url-path to the Edit page
+ // for your Plugin's objects, within the "app" basepath.
+ // For instance :
+ // appId = "myplugin"
+ // app.basepath is then "/app/myplugin"
+ // then
+ // editUrlPathFn: (obj) => `#/edit/${obj.id}`
+ //
+ // At onClick of rendered table "edit" link for item {id: 'abc123', ...}, the navigated path will be:
+ // "http://../app/myplugin#/edit/abc123"
+ editUrlPathFn: (obj: SavedObjectType) => string;
+}
diff --git a/src/plugins/opensearch_dashboards_react/public/table_list_view/README.md b/src/plugins/opensearch_dashboards_react/public/table_list_view/README.md
new file mode 100644
index 00000000000..ecbd04a4944
--- /dev/null
+++ b/src/plugins/opensearch_dashboards_react/public/table_list_view/README.md
@@ -0,0 +1,44 @@
+# TableListView
+An OpenDashboardsReact component
+
+## Overview
+TableListView is a component composed of several OUI modules, wrapped in a convenient way for flexibility of options and input data.
+
+The TableListView contains :
+- OUI InMemoryTable, including pagination and sortable columns
+- OUI SearchBar
+- Create button child component or callback handler.
+
+## Props
+- createButton - JSX.Element (optinoal)
+
+ - if provided, this element will be rendered at Right of SearchBox. Element component and callback-handling is expected to be controlled by the component wrapping this TableListView.
+
+- createItem - Function () => void (optional)
+
+ - if provided, and no `createButton`, a default "Create" button will be rendered, using this prop function as callback to the default