diff --git a/examples/portable_dashboards_example/public/app.tsx b/examples/portable_dashboards_example/public/app.tsx index f6baa63305b35..aad020a13af2c 100644 --- a/examples/portable_dashboards_example/public/app.tsx +++ b/examples/portable_dashboards_example/public/app.tsx @@ -14,7 +14,10 @@ import { Redirect } from 'react-router-dom'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { DashboardListingTable } from '@kbn/dashboard-plugin/public'; +import { + DashboardListingTable, + type DashboardListingViewRegistry, +} from '@kbn/dashboard-plugin/public'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; @@ -125,13 +128,14 @@ const DashboardsDemo = ({ }; const PortableDashboardListingDemo = ({ history }: { history: AppMountParameters['history'] }) => { + const listingViewRegistry: DashboardListingViewRegistry = new Set(); return ( alert(`Here's where I would redirect you to ${dashboardId ?? 'a new Dashboard'}`) } getDashboardUrl={() => 'https://www.elastic.co/'} - listingViewRegistry={new Set()} + listingViewRegistry={listingViewRegistry} > history.push(DASHBOARD_DEMO_PATH)}> Go back to usage demos diff --git a/package.json b/package.json index b38ba5bbff030..5a844a668f658 100644 --- a/package.json +++ b/package.json @@ -1157,6 +1157,7 @@ "@kbn/vis-type-vega-plugin": "link:src/platform/plugins/private/vis_types/vega", "@kbn/vis-type-vislib-plugin": "link:src/platform/plugins/private/vis_types/vislib", "@kbn/vis-type-xy-plugin": "link:src/platform/plugins/private/vis_types/xy", + "@kbn/visualization-listing-plugin": "link:src/platform/plugins/private/visualization_listing", "@kbn/visualization-ui-components": "link:src/platform/packages/shared/kbn-visualization-ui-components", "@kbn/visualization-utils": "link:src/platform/packages/shared/kbn-visualization-utils", "@kbn/visualizations-common": "link:src/platform/packages/shared/kbn-visualizations-common", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 410560cd37081..f12caf2eeea01 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -204,7 +204,8 @@ pageLoadAssetSize: visTypeVega: 38538 visTypeVislib: 14679 visTypeXy: 32342 - visualizations: 53333 + visualizationListing: 10000 + visualizations: 38375 watcher: 10485 workflowsExtensions: 61264 workflowsManagement: 8497 diff --git a/src/platform/plugins/private/event_annotation_listing/public/plugin.ts b/src/platform/plugins/private/event_annotation_listing/public/plugin.ts index b45a5a1b40534..415c317ea8ec9 100644 --- a/src/platform/plugins/private/event_annotation_listing/public/plugin.ts +++ b/src/platform/plugins/private/event_annotation_listing/public/plugin.ts @@ -38,7 +38,7 @@ export interface EventAnnotationListingStartDependencies { interface SetupDependencies { visualizations: VisualizationsSetup; - dashboard: DashboardSetup; + dashboard?: DashboardSetup; } /** @public */ @@ -97,7 +97,9 @@ export class EventAnnotationListingPlugin }, }; dependencies.visualizations.listingViewRegistry.add(annotationGroupsTabConfig); - dependencies.dashboard.listingViewRegistry.add(annotationGroupsTabConfig); + if (dependencies.dashboard) { + dependencies.dashboard.listingViewRegistry.add(annotationGroupsTabConfig); + } } public start(core: CoreStart, plugins: object): void { diff --git a/src/platform/plugins/private/visualization_listing/README.md b/src/platform/plugins/private/visualization_listing/README.md new file mode 100644 index 0000000000000..7957090df5fe5 --- /dev/null +++ b/src/platform/plugins/private/visualization_listing/README.md @@ -0,0 +1,42 @@ +# Visualization Listing Plugin + +This plugin handles the integration between the Visualizations and Dashboard plugins by registering the Visualizations tab in the Dashboard listing view. + +## Pattern + +This follows the same pattern as `eventAnnotationListing`, which integrates annotation groups into both visualizations and dashboard listing views. + +### Plugin Dependencies + +**Required:** + +- `visualizations` - provides the tab content and functionality +- `embeddable` - needed for visualizations rendering + +**Optional:** + +- `dashboard` - receives the tab registration (gracefully skipped if not present) +- `savedObjectsTaggingOss` - for tagging functionality + +## Implementation + +In `setup()`: + +```typescript +if (dependencies.dashboard) { + dependencies.dashboard.listingViewRegistry.add(visualizationsTabConfig); +} +``` + +The tab configuration dynamically imports from: + +```typescript +'@kbn/visualizations-plugin/public/visualization_listing'; +``` + +This import works because visualizations exposes this directory via `extraPublicDirs` in its `kibana.jsonc`. + +## Related Plugins + +- `@kbn/visualizations-plugin` - provides the visualization listing tab component +- `@kbn/dashboard-plugin` - provides the listing view registry diff --git a/src/platform/plugins/private/visualization_listing/kibana.jsonc b/src/platform/plugins/private/visualization_listing/kibana.jsonc new file mode 100644 index 0000000000000..d19c45a78ce26 --- /dev/null +++ b/src/platform/plugins/private/visualization_listing/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/visualization-listing-plugin", + "owner": ["@elastic/kibana-visualizations"], + "group": "platform", + "visibility": "private", + "description": "The listing page integration for visualizations in dashboard app.", + "plugin": { + "id": "visualizationListing", + "browser": true, + "server": false, + "requiredPlugins": ["visualizations", "embeddable", "contentManagement"], + "optionalPlugins": ["dashboard", "savedObjectsTaggingOss"], + "requiredBundles": [], + "extraPublicDirs": [] + } +} diff --git a/src/platform/plugins/private/visualization_listing/public/index.ts b/src/platform/plugins/private/visualization_listing/public/index.ts new file mode 100644 index 0000000000000..d62f93724b5f6 --- /dev/null +++ b/src/platform/plugins/private/visualization_listing/public/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { PluginInitializerContext } from '@kbn/core/public'; +import { VisualizationListingPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new VisualizationListingPlugin(); +} + +export type { VisualizationListingPluginSetup, VisualizationListingPluginStart } from './plugin'; diff --git a/src/platform/plugins/private/visualization_listing/public/plugin.ts b/src/platform/plugins/private/visualization_listing/public/plugin.ts new file mode 100644 index 0000000000000..443161cd38840 --- /dev/null +++ b/src/platform/plugins/private/visualization_listing/public/plugin.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Plugin, CoreSetup, CoreStart } from '@kbn/core/public'; +import type { VisualizationsSetup, VisualizationsStart } from '@kbn/visualizations-plugin/public'; +import type { DashboardSetup } from '@kbn/dashboard-plugin/public'; +import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { i18n } from '@kbn/i18n'; +import type { TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view'; + +interface SetupDependencies { + visualizations: VisualizationsSetup; + dashboard?: DashboardSetup; +} + +interface StartDependencies { + visualizations: VisualizationsStart; + embeddable: EmbeddableStart; + contentManagement: ContentManagementPublicStart; + savedObjectsTaggingOss?: { + getTaggingApi: () => SavedObjectsTaggingApi; + }; +} + +/** @public */ +export type VisualizationListingPluginSetup = void; +export type VisualizationListingPluginStart = void; + +/** @public */ +export class VisualizationListingPlugin + implements + Plugin< + VisualizationListingPluginSetup, + VisualizationListingPluginStart, + SetupDependencies, + StartDependencies + > +{ + public setup(core: CoreSetup, dependencies: SetupDependencies) { + // Register visualizations tab with Dashboard's listing view + if (dependencies.dashboard) { + const visualizationsTabConfig = { + title: i18n.translate('visualizationListing.dashboardListingTab.title', { + defaultMessage: 'Visualizations', + }), + id: 'visualizations', + getTableList: async (props: TableListTabParentProps) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { GetVisualizationsTableList } = await import( + '@kbn/visualizations-plugin/public/visualization_listing' + ); + + return GetVisualizationsTableList(props, { + core: coreStart, + embeddable: pluginsStart.embeddable, + savedObjectsTagging: pluginsStart.savedObjectsTaggingOss?.getTaggingApi(), + contentManagement: pluginsStart.contentManagement, + visualizeCapabilities: coreStart.application.capabilities.visualize_v2, + showNewVisModal: pluginsStart.visualizations.showNewVisModal, + navigateToVisualization: (stateTransfer: any, id: string) => { + stateTransfer.navigateToEditor('visualize', { + path: id, + state: { + originatingApp: '', + }, + }); + }, + visualizationsService: { + findListItems: pluginsStart.visualizations.findListItems, + getTypes: () => pluginsStart.visualizations, + }, + }); + }, + }; + + dependencies.dashboard.listingViewRegistry.add(visualizationsTabConfig); + } + } + + public start(core: CoreStart, plugins: object): void {} +} diff --git a/src/platform/plugins/private/visualization_listing/tsconfig.json b/src/platform/plugins/private/visualization_listing/tsconfig.json new file mode 100644 index 0000000000000..527500daf6d16 --- /dev/null +++ b/src/platform/plugins/private/visualization_listing/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@kbn/tsconfig-base/tsconfig.json", + "compilerOptions": { + "outDir": "target/types" + }, + "include": ["public/**/*"], + "kbn_references": [ + "@kbn/core", + "@kbn/visualizations-plugin", + "@kbn/dashboard-plugin", + "@kbn/embeddable-plugin", + "@kbn/saved-objects-tagging-oss-plugin", + "@kbn/i18n", + "@kbn/content-management-tabbed-table-list-view" + ], + "exclude": ["target/**/*"] +} diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index ff09c3f17b32e..e70d683aecbbc 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -14,7 +14,6 @@ "dataViews", "dataViewEditor", "embeddable", - "eventAnnotation", "fieldFormats", "controls", "inspector", @@ -27,8 +26,7 @@ "uiActions", "urlForwarding", "presentationUtil", - "unifiedSearch", - "visualizations" + "unifiedSearch" ], "optionalPlugins": [ "home", @@ -42,7 +40,8 @@ "noDataPage", "observabilityAIAssistant", "lens", - "cps" + "cps", + "visualizations" ], "requiredBundles": [ "kibanaReact", diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx index fe4d8a2d1a705..0f20bf97e21da 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; @@ -22,7 +22,7 @@ import { import { getDashboardListItemLink } from './get_dashboard_list_item_link'; import type { DashboardRedirect } from '../types'; import { findService } from '../../dashboard_client'; -import { DashboardMountContext } from '../hooks/dashboard_mount_context'; +import { useDashboardMountContext } from '../hooks/dashboard_mount_context'; export interface DashboardListingPageProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -37,7 +37,7 @@ export const DashboardListingPage = ({ initialFilter, kbnUrlStateStorage, }: DashboardListingPageProps) => { - const { listingViewRegistry } = useContext(DashboardMountContext); + const { listingViewRegistry } = useDashboardMountContext(); const [showNoDataPage, setShowNoDataPage] = useState(); useEffect(() => { let isMounted = true; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing.tsx index 967bb925110de..0adf31936c0b9 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing.tsx @@ -19,7 +19,8 @@ import { QueryClientProvider } from '@kbn/react-query'; import { coreServices } from '../services/kibana_services'; import { dashboardQueryClient } from '../services/dashboard_query_client'; import { getDashboardListingTabs } from './get_dashboard_listing_tabs'; -import { TAB_IDS, type DashboardListingProps } from './types'; +import { type DashboardListingProps } from './types'; +import { DASHBOARD_APP_ID } from '../../common/page_bundle_constants'; export const DashboardListing = ({ children, @@ -54,12 +55,9 @@ export const DashboardListing = ({ ] ); - const activeTabId = useMemo(() => { - const validTabIds = tabs.map((tab) => tab.id); - return activeTabParam && validTabIds.includes(activeTabParam) - ? activeTabParam - : TAB_IDS.DASHBOARDS; - }, [activeTabParam, tabs]); + const activeTabId = tabs.some((tab) => tab.id === activeTabParam) + ? activeTabParam! + : DASHBOARD_APP_ID; const changeActiveTab = useCallback((tabId: string) => { coreServices.application.navigateToUrl(`#/list/${tabId}`); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx index 048dc1466259e..e2a03fc1cad24 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx @@ -31,10 +31,9 @@ const renderDashboardListingEmptyPrompt = (props: Partial, { wrapper: I18nProvider } @@ -58,10 +57,13 @@ test.each([ } ); -test('renders disabled action button when disableCreateDashboardButton is true', async () => { - (coreServices.application.capabilities as any).dashboard_v2.showWriteControls = true; - renderDashboardListingEmptyPrompt({ disableCreateDashboardButton: true }); - expect(screen.getByTestId('newItemButton')).toBeDisabled(); +test('renders disabled action button when dashboard capabilities do not allow creation', async () => { + // Set capabilities to not allow writes + (coreServices.application.capabilities as any).dashboard_v2.showWriteControls = false; + renderDashboardListingEmptyPrompt({}); + // Button should not be rendered when showWriteControls is false + const button = screen.queryByTestId('newItemButton'); + expect(button).not.toBeInTheDocument(); }); test('renders continue button when no dashboards exist but one is in progress', async () => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx index 2a30de55c64b9..cb9e54f08e569 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx @@ -34,20 +34,18 @@ import type { DashboardListingProps } from './types'; export interface DashboardListingEmptyPromptProps { createItem: () => void; - disableCreateDashboardButton?: boolean; unsavedDashboardIds: string[]; goToDashboard: DashboardListingProps['goToDashboard']; - setUnsavedDashboardIds: React.Dispatch>; + refreshUnsavedDashboards: () => void; useSessionStorageIntegration: DashboardListingProps['useSessionStorageIntegration']; } export const DashboardListingEmptyPrompt = ({ useSessionStorageIntegration, - setUnsavedDashboardIds, + refreshUnsavedDashboards, unsavedDashboardIds, goToDashboard, createItem, - disableCreateDashboardButton, }: DashboardListingEmptyPromptProps) => { const isEditingFirstDashboard = useMemo( () => useSessionStorageIntegration && unsavedDashboardIds.length === 1, @@ -57,13 +55,7 @@ export const DashboardListingEmptyPrompt = ({ const getEmptyAction = useCallback(() => { if (!isEditingFirstDashboard) { return ( - + {noItemsStrings.getCreateNewDashboardText()} ); @@ -78,7 +70,7 @@ export const DashboardListingEmptyPrompt = ({ confirmDiscardUnsavedChanges(() => { const dashboardBackupService = getDashboardBackupService(); dashboardBackupService.clearState(DASHBOARD_PANELS_UNSAVED_ID); - setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); + refreshUnsavedDashboards(); }) } data-test-subj="discardDashboardPromptButton" @@ -101,13 +93,7 @@ export const DashboardListingEmptyPrompt = ({ ); - }, [ - isEditingFirstDashboard, - createItem, - disableCreateDashboardButton, - goToDashboard, - setUnsavedDashboardIds, - ]); + }, [isEditingFirstDashboard, createItem, goToDashboard, refreshUnsavedDashboards]); if (!getDashboardCapabilities().showWriteControls) { return ( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_table.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_table.tsx index f9dd570e193a0..6ded3a23171ea 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_table.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_table.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { useMemo, useCallback } from 'react'; import { TableListViewKibanaProvider, @@ -21,9 +21,13 @@ import { savedObjectsTaggingService, serverlessService, } from '../services/kibana_services'; +import { getDashboardBackupService } from '../services/dashboard_backup_service'; +import { confirmCreateWithUnsaved } from './confirm_overlays'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; +import { DashboardListingEmptyPrompt } from './dashboard_listing_empty_prompt'; import { useDashboardListingTable } from './hooks/use_dashboard_listing_table'; -import type { DashboardListingProps, DashboardListingUserContent } from './types'; +import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; +import type { DashboardListingProps, DashboardSavedObjectUserContent } from './types'; export const DashboardListingTable = ({ disableCreateDashboardButton, @@ -45,15 +49,37 @@ export const DashboardListingTable = ({ tableListViewTableProps: { title: tableCaption, ...tableListViewTable }, contentInsightsClient, } = useDashboardListingTable({ - disableCreateDashboardButton, goToDashboard, getDashboardUrl, urlStateEnabled, - useSessionStorageIntegration, initialFilter, - showCreateDashboardButton, }); + const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); + + const createItem = useCallback(() => { + if (useSessionStorageIntegration && dashboardBackupService.dashboardHasUnsavedEdits()) { + confirmCreateWithUnsaved(() => { + dashboardBackupService.clearState(); + goToDashboard(); + }, goToDashboard); + return; + } + goToDashboard(); + }, [dashboardBackupService, goToDashboard, useSessionStorageIntegration]); + + const { showWriteControls } = getDashboardCapabilities(); + + const emptyPrompt = ( + + ); + return ( - + tableCaption={tableCaption} {...tableListViewTable} + createItem={ + !showWriteControls || !showCreateDashboardButton || disableCreateDashboardButton + ? undefined + : createItem + } + emptyPrompt={emptyPrompt} onFetchSuccess={() => {}} setPageDataTestSubject={() => {}} /> diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/get_dashboard_listing_tabs.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/get_dashboard_listing_tabs.tsx index b3abe898c4213..74e1822eb29f8 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/get_dashboard_listing_tabs.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/get_dashboard_listing_tabs.tsx @@ -7,9 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useMemo } from 'react'; -import { css } from '@emotion/react'; -import { logicalSizeCSS, useEuiTheme } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import type { TableListTab, @@ -29,9 +27,17 @@ import { serverlessService, usageCollectionService, } from '../services/kibana_services'; +import { getDashboardBackupService } from '../services/dashboard_backup_service'; +import { confirmCreateWithUnsaved } from './confirm_overlays'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; +import { DashboardListingEmptyPrompt } from './dashboard_listing_empty_prompt'; import { useDashboardListingTable } from './hooks/use_dashboard_listing_table'; -import { TAB_IDS, type DashboardListingProps, type DashboardListingUserContent } from './types'; +import { + type DashboardListingProps, + type DashboardListingUserContent, + type DashboardSavedObjectUserContent, +} from './types'; +import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; type GetDashboardListingTabsParams = Pick< DashboardListingProps, @@ -68,11 +74,22 @@ const DashboardsTabContent = ({ } = useDashboardListingTable({ goToDashboard, getDashboardUrl, - useSessionStorageIntegration, initialFilter, - contentTypeFilter: TAB_IDS.DASHBOARDS, }); + const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); + + const createItem = useCallback(() => { + if (useSessionStorageIntegration && dashboardBackupService.dashboardHasUnsavedEdits()) { + confirmCreateWithUnsaved(() => { + dashboardBackupService.clearState(); + goToDashboard(); + }, goToDashboard); + return; + } + goToDashboard(); + }, [dashboardBackupService, goToDashboard, useSessionStorageIntegration]); + const dashboardFavoritesClient = useMemo(() => { return new FavoritesClient(DASHBOARD_APP_ID, DASHBOARD_SAVED_OBJECT_TYPE, { http: coreServices.http, @@ -81,6 +98,18 @@ const DashboardsTabContent = ({ }); }, []); + const { showWriteControls } = getDashboardCapabilities(); + + const emptyPrompt = ( + + ); + return ( - + tableCaption={tableListViewTableProps.title} {...tableListViewTableProps} + createItem={showWriteControls ? createItem : undefined} + emptyPrompt={emptyPrompt} {...parentProps} /> ); }; -const VisualizationsTabContent = ({ - goToDashboard, - getDashboardUrl, - useSessionStorageIntegration, - initialFilter, - parentProps, -}: TabContentProps) => { - const { euiTheme } = useEuiTheme(); - const { tableListViewTableProps } = useDashboardListingTable({ - goToDashboard, - getDashboardUrl, - useSessionStorageIntegration, - initialFilter, - contentTypeFilter: TAB_IDS.VISUALIZATIONS, - }); - - return ( - -
- - tableCaption={tableListViewTableProps.title} - {...tableListViewTableProps} - {...parentProps} - /> -
-
- ); -}; - export const getDashboardListingTabs = ({ goToDashboard, getDashboardUrl, @@ -161,26 +150,15 @@ export const getDashboardListingTabs = ({ title: i18n.translate('dashboard.listing.tabs.dashboards.title', { defaultMessage: 'Dashboards', }), - id: TAB_IDS.DASHBOARDS, + id: DASHBOARD_APP_ID, getTableList: (parentProps) => ( ), }; - const visualizationsTab: TableListTab = { - title: i18n.translate('dashboard.listing.tabs.visualizations.title', { - defaultMessage: 'Visualizations', - }), - id: TAB_IDS.VISUALIZATIONS, - getTableList: (parentProps) => ( - - ), - }; - - // Additional tabs from registry (e.g., annotation groups from Event Annotation Listing plugin) const registryTabs = listingViewRegistry ? Array.from(listingViewRegistry as Set) : []; - return [dashboardsTab, visualizationsTab, ...registryTabs]; + return [dashboardsTab, ...registryTabs]; }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/delete_items.ts b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/delete_items.ts deleted file mode 100644 index 67b7fb5a64791..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/delete_items.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { asyncMap } from '@kbn/std'; -import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; - -import { SAVED_OBJECT_DELETE_TIME } from '../../../utils/telemetry_constants'; -import { coreServices, contentManagementService } from '../../../services/kibana_services'; -import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; -import { dashboardClient } from '../../../dashboard_client'; -import { dashboardListingErrorStrings } from '../../_dashboard_listing_strings'; - -interface DeleteDashboardListingItemsParams { - itemsToDelete: Array<{ id: string; type?: string }>; -} -export async function deleteDashboardListingItems({ - itemsToDelete, -}: DeleteDashboardListingItemsParams): Promise { - if (!itemsToDelete || itemsToDelete.length === 0) { - return; - } - - const dashboardBackupService = getDashboardBackupService(); - - try { - const deleteStartTime = window.performance.now(); - - await asyncMap(itemsToDelete, async ({ id, type }) => { - if (type === 'dashboard') { - await dashboardClient.delete(id); - dashboardBackupService.clearState(id); - } else if (type) { - await contentManagementService.client.delete({ - contentTypeId: type, - id, - }); - } - }); - - reportPerformanceMetricEvent(coreServices.analytics, { - eventName: SAVED_OBJECT_DELETE_TIME, - duration: window.performance.now() - deleteStartTime, - meta: { - saved_object_type: itemsToDelete[0]?.type ?? 'unknown', - total: itemsToDelete.length, - }, - }); - } catch (error) { - coreServices.notifications.toasts.addError(error, { - title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), - }); - } -} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/edit_item.ts b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/edit_item.ts deleted file mode 100644 index acc72aa8d3b72..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/edit_item.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { ViewMode } from '@kbn/presentation-publishing'; - -import { coreServices, embeddableService } from '../../../services/kibana_services'; -import { navigateToVisualization } from './navigation'; -import type { DashboardListingUserContent, DashboardVisualizationUserContent } from '../../types'; - -export async function editDashboardListingItem( - item: DashboardListingUserContent, - goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void -): Promise { - const { id, type } = item; - - if (type === 'dashboard') { - goToDashboard(id, 'edit'); - return; - } - - if (type === 'event-annotation-group') { - // Annotation groups are handled by the annotation groups tab component - return; - } - - const visItem = item as DashboardVisualizationUserContent; - const { editor } = visItem; - - if (editor) { - if ('onEdit' in editor && editor.onEdit) { - await editor.onEdit(id!); - return; - } - - if ('editUrl' in editor) { - const { editApp, editUrl } = editor; - - // Custom app navigation (e.g., Maps) - if (editApp && editUrl) { - coreServices.application.navigateToApp(editApp, { path: editUrl }); - return; - } - - if (editUrl) { - navigateToVisualization(embeddableService.getStateTransfer(), id!, editUrl); - return; - } - } - } - - // Fallback: edit through visualize app - navigateToVisualization(embeddableService.getStateTransfer(), id!); -} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/entity_names.ts b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/entity_names.ts deleted file mode 100644 index 6b9f20e7b86cd..0000000000000 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/entity_names.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { TAB_IDS, type TabId } from '../../types'; -import { dashboardListingTableStrings } from '../../_dashboard_listing_strings'; - -export interface EntityNames { - entityName: string; - entityNamePlural: string; -} - -export const getEntityNames = (contentTypeFilter?: TabId): EntityNames => { - switch (contentTypeFilter) { - case TAB_IDS.VISUALIZATIONS: - return { - entityName: i18n.translate('dashboard.listing.table.visualizationEntityName', { - defaultMessage: 'visualization', - }), - entityNamePlural: i18n.translate('dashboard.listing.table.visualizationEntityNamePlural', { - defaultMessage: 'visualizations', - }), - }; - - case TAB_IDS.ANNOTATIONS: - return { - entityName: i18n.translate('dashboard.listing.table.annotationEntityName', { - defaultMessage: 'annotation group', - }), - entityNamePlural: i18n.translate('dashboard.listing.table.annotationEntityNamePlural', { - defaultMessage: 'annotation groups', - }), - }; - default: - return { - entityName: dashboardListingTableStrings.getEntityName(), - entityNamePlural: dashboardListingTableStrings.getEntityNamePlural(), - }; - } -}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/find_items.ts b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/find_items.ts index adfb738a4271f..772243080339e 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/find_items.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/helpers/find_items.ts @@ -9,23 +9,12 @@ import type { Reference } from '@kbn/content-management-utils'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; -import { toTableListViewSavedObject } from '@kbn/visualizations-plugin/public'; import { SAVED_OBJECT_LOADED_TIME } from '../../../utils/telemetry_constants'; import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -import { - coreServices, - eventAnnotationService, - savedObjectsTaggingService, - visualizationsService, -} from '../../../services/kibana_services'; +import { coreServices, savedObjectsTaggingService } from '../../../services/kibana_services'; import { findService } from '../../../dashboard_client'; -import { - TAB_IDS, - type TabId, - type DashboardListingUserContent, - type DashboardVisualizationUserContent, -} from '../../types'; +import { type DashboardSavedObjectUserContent } from '../../types'; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; @@ -33,65 +22,12 @@ const getReferenceIds = (refs?: Reference[]) => refs?.map((r) => r.id); export async function findDashboardListingItems( searchTerm: string, - tabId: TabId, options?: { references?: Reference[]; referencesToExclude?: Reference[] } -): Promise<{ total: number; hits: DashboardListingUserContent[] }> { +): Promise<{ total: number; hits: DashboardSavedObjectUserContent[] }> { const { references, referencesToExclude } = options ?? {}; const limit = coreServices.uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); const startTime = window.performance.now(); - const reportSearchDuration = (type: string) => { - reportPerformanceMetricEvent(coreServices.analytics, { - eventName: SAVED_OBJECT_LOADED_TIME, - duration: window.performance.now() - startTime, - meta: { saved_object_type: type }, - }); - }; - - if (tabId === TAB_IDS.VISUALIZATIONS) { - const response = await visualizationsService.findListItems( - searchTerm, - limit, - references, - referencesToExclude - ); - reportSearchDuration('visualization'); - - return { - total: response.total, - hits: response.hits.map((hit) => { - const item = toTableListViewSavedObject(hit) as DashboardVisualizationUserContent; - return { ...item, type: item.savedObjectType || item.type }; - }), - }; - } - - if (tabId === TAB_IDS.ANNOTATIONS && eventAnnotationService) { - const service = await eventAnnotationService.getService(); - const response = await service.findAnnotationGroupContent( - searchTerm, - limit, - getReferenceIds(references), - getReferenceIds(referencesToExclude) - ); - reportSearchDuration('event-annotation-group'); - - return { - total: response.total, - hits: response.hits.map((hit) => ({ - ...hit, - type: 'event-annotation-group' as const, - attributes: { - title: hit.attributes.title, - description: hit.attributes.description, - timeRestore: false as const, - indexPatternId: hit.attributes.indexPatternId, - dataViewSpec: hit.attributes.dataViewSpec, - }, - })), - }; - } - const { total, dashboards } = await findService.search({ search: searchTerm, per_page: limit, @@ -100,7 +36,12 @@ export async function findDashboardListingItems( excluded: getReferenceIds(referencesToExclude) ?? [], }, }); - reportSearchDuration(DASHBOARD_SAVED_OBJECT_TYPE); + + reportPerformanceMetricEvent(coreServices.analytics, { + eventName: SAVED_OBJECT_LOADED_TIME, + duration: window.performance.now() - startTime, + meta: { saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE }, + }); const tagApi = savedObjectsTaggingService?.getTaggingApi(); return { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index 67dd21e5b1f7c..38f8c3fb07760 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -11,7 +11,6 @@ import { renderHook, act } from '@testing-library/react'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { coreServices } from '../../services/kibana_services'; -import { confirmCreateWithUnsaved } from '../confirm_overlays'; import type { DashboardSavedObjectUserContent } from '../types'; import { useDashboardListingTable } from './use_dashboard_listing_table'; @@ -88,22 +87,6 @@ describe('useDashboardListingTable', () => { expect(result.current.unsavedDashboardIds).toEqual([]); }); - test('should not render the create dashboard button when showCreateDashboardButton is false', () => { - const initialFilter = 'myFilter'; - const { result } = renderHook(() => - useDashboardListingTable({ - getDashboardUrl, - goToDashboard, - initialFilter, - urlStateEnabled: false, - showCreateDashboardButton: false, - }) - ); - - const tableListViewTableProps = result.current.tableListViewTableProps; - expect(tableListViewTableProps.createItem).toBeUndefined(); - }); - test('should return the correct tableListViewTableProps', () => { const initialFilter = 'myFilter'; const { result } = renderHook(() => @@ -118,10 +101,8 @@ describe('useDashboardListingTable', () => { const tableListViewTableProps = result.current.tableListViewTableProps; const expectedProps = { - createItem: expect.any(Function), deleteItems: expect.any(Function), editItem: expect.any(Function), - noItemsMessage: expect.any(Object), entityName: 'Dashboard', entityNamePlural: 'Dashboards', findItems: expect.any(Function), @@ -179,40 +160,7 @@ describe('useDashboardListingTable', () => { expect(goToDashboard).toHaveBeenCalled(); }); - test('should call goToDashboard when createItem is called without unsaved changes', () => { - const { result } = renderHook(() => - useDashboardListingTable({ - getDashboardUrl, - goToDashboard, - }) - ); - - act(() => { - result.current.tableListViewTableProps.createItem?.(); - }); - - expect(goToDashboard).toHaveBeenCalled(); - }); - - test('should call confirmCreateWithUnsaved and clear state when createItem is called with unsaved changes', () => { - const { result } = renderHook(() => - useDashboardListingTable({ - getDashboardUrl, - goToDashboard, - useSessionStorageIntegration: true, - }) - ); - - act(() => { - result.current.tableListViewTableProps.createItem?.(); - }); - - expect(confirmCreateWithUnsaved).toHaveBeenCalled(); - expect(clearStateMock).toHaveBeenCalled(); - expect(goToDashboard).toHaveBeenCalled(); - }); - - test('createItem should be undefined when showWriteControls equals false', () => { + test('createItem should not be provided by hook - tabs provide their own', () => { coreServices.application.capabilities = { ...coreServices.application.capabilities, dashboard_v2: { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index c9493325802e6..f52feffb95b93 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -7,59 +7,42 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import type { EuiBasicTableColumn } from '@elastic/eui'; +import { useCallback, useMemo, useState } from 'react'; import type { OpenContentEditorParams } from '@kbn/content-management-content-editor'; import { ContentInsightsClient } from '@kbn/content-management-content-insights-public'; import type { TableListViewTableProps } from '@kbn/content-management-table-list-view-table'; import type { Reference } from '@kbn/content-management-utils'; import type { ViewMode } from '@kbn/presentation-publishing'; +import { asyncMap } from '@kbn/std'; +import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; +import { DASHBOARD_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { getDashboardRecentlyAccessedService } from '../../services/dashboard_recently_accessed_service'; -import { - coreServices, - embeddableService, - visualizationsService, -} from '../../services/kibana_services'; +import { coreServices } from '../../services/kibana_services'; import { logger } from '../../services/logger'; import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities'; +import { SAVED_OBJECT_DELETE_TIME } from '../../utils/telemetry_constants'; import { dashboardListingErrorStrings, dashboardListingTableStrings, } from '../_dashboard_listing_strings'; -import { confirmCreateWithUnsaved } from '../confirm_overlays'; -import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt'; -import { - TAB_IDS, - type TabId, - type DashboardListingUserContent, - type DashboardVisualizationUserContent, - type DashboardSavedObjectUserContent, -} from '../types'; +import { type DashboardSavedObjectUserContent } from '../types'; import { checkForDuplicateDashboardTitle, dashboardClient, findService, } from '../../dashboard_client'; import { findDashboardListingItems } from './helpers/find_items'; -import { deleteDashboardListingItems } from './helpers/delete_items'; -import { editDashboardListingItem } from './helpers/edit_item'; -import { navigateToVisualization } from './helpers/navigation'; -import { getEntityNames } from './helpers/entity_names'; -import { - getVisualizationListingColumn, - getVisualizationListingEmptyPrompt, -} from '../utils/visualization_listing_helpers'; -type GetDetailViewLink = TableListViewTableProps['getDetailViewLink']; +type GetDetailViewLink = + TableListViewTableProps['getDetailViewLink']; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; type DashboardListingViewTableProps = Omit< - TableListViewTableProps, + TableListViewTableProps, 'tableCaption' | 'onFetchSuccess' | 'setPageDataTestSubject' > & { title: string }; @@ -72,31 +55,23 @@ interface UseDashboardListingTableReturnType { export const useDashboardListingTable = ({ dashboardListingId = 'dashboard', - disableCreateDashboardButton, getDashboardUrl, goToDashboard, headingId = 'dashboardListingHeading', initialFilter, urlStateEnabled, - useSessionStorageIntegration, - showCreateDashboardButton = true, - contentTypeFilter, }: { dashboardListingId?: string; - disableCreateDashboardButton?: boolean; getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; headingId?: string; initialFilter?: string; urlStateEnabled?: boolean; - useSessionStorageIntegration?: boolean; - showCreateDashboardButton?: boolean; - contentTypeFilter?: TabId; }): UseDashboardListingTableReturnType => { - const { getTableListTitle } = dashboardListingTableStrings; - - const { entityName, entityNamePlural } = getEntityNames(contentTypeFilter); + const { getTableListTitle, getEntityName, getEntityNamePlural } = dashboardListingTableStrings; + const entityName = getEntityName(); + const entityNamePlural = getEntityNamePlural(); const title = getTableListTitle(); const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); @@ -108,44 +83,6 @@ export const useDashboardListingTable = ({ const listingLimit = coreServices.uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = coreServices.uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); - // Store close function for new visualization modal (to close on navigation) - const closeNewVisModal = useRef(() => {}); - const { pathname } = useLocation(); - - const createItem = useCallback( - (contentTypeTab?: TabId) => { - const contentType = contentTypeTab ?? contentTypeFilter; - - switch (contentType) { - case TAB_IDS.VISUALIZATIONS: - closeNewVisModal.current = visualizationsService.showNewVisModal(); - return; - - case TAB_IDS.ANNOTATIONS: - coreServices.application.navigateToApp('lens', { path: '#/' }); - return; - - case TAB_IDS.DASHBOARDS: - default: - if (useSessionStorageIntegration && dashboardBackupService.dashboardHasUnsavedEdits()) { - confirmCreateWithUnsaved(() => { - dashboardBackupService.clearState(); - goToDashboard(); - }, goToDashboard); - return; - } - goToDashboard(); - } - }, - [dashboardBackupService, goToDashboard, useSessionStorageIntegration, contentTypeFilter] - ); - - useEffect(() => { - return () => { - closeNewVisModal.current(); - }; - }, [pathname]); - const updateItemMeta = useCallback( async ({ id, ...updatedState }: Parameters['onSave']>[0]) => { const dashboard = await findService.findById(id); @@ -202,79 +139,55 @@ export const useDashboardListingTable = ({ ); const deleteItems = useCallback( - async (itemsToDelete: Array<{ id: string; type?: string }>) => { - await deleteDashboardListingItems({ itemsToDelete }); + async (dashboardsToDelete: Array<{ id: string }>) => { + try { + const deleteStartTime = window.performance.now(); + + await asyncMap(dashboardsToDelete, async ({ id }) => { + await dashboardClient.delete(id); + dashboardBackupService.clearState(id); + }); + + const deleteDuration = window.performance.now() - deleteStartTime; + reportPerformanceMetricEvent(coreServices.analytics, { + eventName: SAVED_OBJECT_DELETE_TIME, + duration: deleteDuration, + meta: { + saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE, + total: dashboardsToDelete.length, + }, + }); + } catch (error) { + coreServices.notifications.toasts.addError(error, { + title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), + }); + } + setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); }, [dashboardBackupService] ); const editItem = useCallback( - (item: DashboardListingUserContent) => editDashboardListingItem(item, goToDashboard), + ({ id }: { id: string | undefined }) => goToDashboard(id, 'edit'), [goToDashboard] ); const getDetailViewLink = useCallback>( - (entity: DashboardListingUserContent) => { - if (entity.type !== 'dashboard') { - return undefined; - } - - const dashboard = entity as DashboardSavedObjectUserContent; - return getDashboardUrl(dashboard.id, dashboard.attributes.timeRestore); - }, + ({ id, attributes: { timeRestore } }) => getDashboardUrl(id, timeRestore), [getDashboardUrl] ); - const getOnClickTitle = useCallback((item: DashboardListingUserContent) => { - const { id, type } = item; - - // Dashboards: let the link handle it (no onClick needed) - if (type === 'dashboard') { - return undefined; - } - - // Annotation groups are view-only - don't allow clicking - if (type === 'event-annotation-group') { - return undefined; - } - - // Handle visualizations (including lens, maps, links, etc.) - const visItem = item as DashboardVisualizationUserContent; - - // Don't allow clicking on read-only visualizations - if (visItem.attributes.readOnly) { - return undefined; - } - - // Use editor config if available (e.g., maps have editApp='maps') - const { editor } = visItem; - if (editor && 'editUrl' in editor && editor.editApp) { - return () => - coreServices.application.navigateToApp(editor.editApp!, { path: editor.editUrl }); - } - - // Default: open in visualize app - return () => navigateToVisualization(embeddableService.getStateTransfer(), id!); - }, []); - - const rowItemActions = useCallback((item: DashboardListingUserContent) => { + const rowItemActions = useCallback((item: DashboardSavedObjectUserContent) => { const { showWriteControls } = getDashboardCapabilities(); - const { managed, type } = item; - const isReadOnlyVisualization = - type !== 'dashboard' && - type !== 'event-annotation-group' && - (item as DashboardVisualizationUserContent).attributes.readOnly; + const { managed } = item; - // Disable edit for managed items or read-only visualizations - if (!showWriteControls || managed || isReadOnlyVisualization) { + if (!showWriteControls || managed) { return { edit: { enabled: false, reason: managed ? dashboardListingTableStrings.getManagementItemDisabledEditMessage() - : isReadOnlyVisualization - ? dashboardListingTableStrings.getReadOnlyVisualizationMessage() : undefined, }, }; @@ -287,45 +200,24 @@ export const useDashboardListingTable = ({ ( searchTerm: string, options?: { references?: Reference[]; referencesToExclude?: Reference[] } - ) => findDashboardListingItems(searchTerm, contentTypeFilter ?? TAB_IDS.DASHBOARDS, options), - [contentTypeFilter] + ) => findDashboardListingItems(searchTerm, options), + [] ); const tableListViewTableProps: DashboardListingViewTableProps = useMemo(() => { const { showWriteControls } = getDashboardCapabilities(); return { - contentEditor: - contentTypeFilter === TAB_IDS.DASHBOARDS - ? { - isReadonly: !showWriteControls, - onSave: updateItemMeta, - customValidators: contentEditorValidators, - } - : { - enabled: false, - }, - createItem: !showWriteControls || !showCreateDashboardButton ? undefined : createItem, + contentEditor: { + isReadonly: !showWriteControls, + onSave: updateItemMeta, + customValidators: contentEditorValidators, + }, deleteItems: !showWriteControls ? undefined : deleteItems, editItem: !showWriteControls ? undefined : editItem, - emptyPrompt: - contentTypeFilter === TAB_IDS.VISUALIZATIONS || - contentTypeFilter === TAB_IDS.ANNOTATIONS ? ( - getVisualizationListingEmptyPrompt(createItem) - ) : ( - - ), entityName, entityNamePlural, findItems, getDetailViewLink, - getOnClickTitle, rowItemActions, headingId, id: dashboardListingId, @@ -334,38 +226,26 @@ export const useDashboardListingTable = ({ listingLimit, title, urlStateEnabled, - createdByEnabled: contentTypeFilter === TAB_IDS.DASHBOARDS, + createdByEnabled: true, recentlyAccessed: getDashboardRecentlyAccessedService(), - customTableColumn: - contentTypeFilter === TAB_IDS.VISUALIZATIONS - ? (getVisualizationListingColumn() as EuiBasicTableColumn) - : undefined, }; }, [ contentEditorValidators, - contentTypeFilter, - createItem, dashboardListingId, deleteItems, - disableCreateDashboardButton, editItem, entityName, entityNamePlural, - goToDashboard, findItems, getDetailViewLink, - getOnClickTitle, headingId, initialFilter, initialPageSize, listingLimit, rowItemActions, - showCreateDashboardButton, title, - unsavedDashboardIds, updateItemMeta, urlStateEnabled, - useSessionStorageIntegration, ]); const refreshUnsavedDashboards = useCallback( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_listing/types.ts index fc921d0f91186..49395efe068a6 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/types.ts @@ -10,19 +10,8 @@ import type { PropsWithChildren } from 'react'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; import type { ViewMode } from '@kbn/presentation-publishing'; -import type { VisualizationListItem, VisualizationStage } from '@kbn/visualizations-plugin/public'; import type { DashboardListingViewRegistry } from '../plugin'; -export type { VisualizationListItem, VisualizationStage }; - -export const TAB_IDS = { - DASHBOARDS: 'dashboards', - VISUALIZATIONS: 'visualizations', - ANNOTATIONS: 'annotations', -} as const; - -export type TabId = (typeof TAB_IDS)[keyof typeof TAB_IDS]; - export type DashboardListingProps = PropsWithChildren<{ disableCreateDashboardButton?: boolean; initialFilter?: string; @@ -34,47 +23,11 @@ export type DashboardListingProps = PropsWithChildren<{ listingViewRegistry: DashboardListingViewRegistry; }>; -interface DashboardListingItemBase extends UserContentCommonSchema { - managed?: boolean; - attributes: { - title: string; - description?: string; - }; -} - -export interface DashboardSavedObjectUserContent extends DashboardListingItemBase { +export interface DashboardSavedObjectUserContent extends UserContentCommonSchema { type: 'dashboard'; - attributes: DashboardListingItemBase['attributes'] & { + attributes: UserContentCommonSchema['attributes'] & { timeRestore: boolean; }; } -export interface DashboardVisualizationUserContent extends DashboardListingItemBase { - type: string; - icon: string; - savedObjectType: string; - title: string; - typeTitle: string; - image?: string; - stage?: VisualizationStage; - error?: string; - editor?: VisualizationListItem['editor']; - attributes: DashboardListingItemBase['attributes'] & { - visType?: string; - readOnly?: boolean; - }; -} - -export interface DashboardAnnotationGroupUserContent extends DashboardListingItemBase { - type: 'event-annotation-group'; - attributes: DashboardListingItemBase['attributes'] & { - timeRestore: false; - indexPatternId?: string; - dataViewSpec?: { id?: string; name?: string }; - }; -} - -export type DashboardListingUserContent = - | DashboardSavedObjectUserContent - | DashboardVisualizationUserContent - | DashboardAnnotationGroupUserContent; +export type DashboardListingUserContent = DashboardSavedObjectUserContent | UserContentCommonSchema; diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index 433dd0bc9ce1c..7c3632c2be4b9 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -102,7 +102,7 @@ export interface DashboardStartDependencies { data: DataPublicPluginStart; dataViewEditor: DataViewEditorStart; embeddable: EmbeddableStart; - eventAnnotation: EventAnnotationService; + eventAnnotation?: EventAnnotationService; fieldFormats: FieldFormatsStart; inspector: InspectorStartContract; navigation: NavigationPublicPluginStart; @@ -117,7 +117,7 @@ export interface DashboardStartDependencies { unifiedSearch: UnifiedSearchPublicPluginStart; urlForwarding: UrlForwardingStart; usageCollection?: UsageCollectionStart; - visualizations: VisualizationsStart; + visualizations?: VisualizationsStart; customBranding: CustomBrandingStart; serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index eb7a3ab32fa2a..3af7153c81b5b 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -40,7 +40,7 @@ export let contentManagementService: ContentManagementPublicStart; export let dataService: DataPublicPluginStart; export let dataViewEditorService: DataViewEditorStart; export let embeddableService: EmbeddableStart; -export let eventAnnotationService: EventAnnotationService; +export let eventAnnotationService: EventAnnotationService | undefined; export let fieldFormatService: FieldFormatsStart; export let navigationService: NavigationPublicPluginStart; export let noDataPageService: NoDataPagePluginStart | undefined; @@ -55,7 +55,7 @@ export let spacesService: SpacesApi | undefined; export let uiActionsService: UiActionsPublicStart; export let urlForwardingService: UrlForwardingStart; export let usageCollectionService: UsageCollectionStart | undefined; -export let visualizationsService: VisualizationsStart; +export let visualizationsService: VisualizationsStart | undefined; const servicesReady$ = new BehaviorSubject(false); diff --git a/src/platform/plugins/shared/visualizations/kibana.jsonc b/src/platform/plugins/shared/visualizations/kibana.jsonc index 33817bed4357f..4f3e0130fe2cf 100644 --- a/src/platform/plugins/shared/visualizations/kibana.jsonc +++ b/src/platform/plugins/shared/visualizations/kibana.jsonc @@ -1,9 +1,7 @@ { "type": "plugin", "id": "@kbn/visualizations-plugin", - "owner": [ - "@elastic/kibana-visualizations" - ], + "owner": ["@elastic/kibana-visualizations"], "group": "platform", "visibility": "shared", "description": "Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable.", @@ -52,7 +50,8 @@ "common/constants", "common/utils", "common/expression_functions", - "common/convert_to_lens" + "common/convert_to_lens", + "public/visualization_listing" ] } } \ No newline at end of file diff --git a/src/platform/plugins/shared/visualizations/public/index.ts b/src/platform/plugins/shared/visualizations/public/index.ts index f312ebb4c765b..d2d69c276a0eb 100644 --- a/src/platform/plugins/shared/visualizations/public/index.ts +++ b/src/platform/plugins/shared/visualizations/public/index.ts @@ -75,4 +75,4 @@ export const getConvertToLensModule = async () => { }; export { getDataViewByIndexPatternId } from './convert_to_lens/datasource'; -export { toTableListViewSavedObject } from './utils/to_table_list_view_saved_object'; +export { type VisualizeUserContent } from './visualization_listing/find_visualizations'; diff --git a/src/platform/plugins/shared/visualizations/public/utils/to_table_list_view_saved_object.ts b/src/platform/plugins/shared/visualizations/public/utils/to_table_list_view_saved_object.ts deleted file mode 100644 index 1ee0d60c403fa..0000000000000 --- a/src/platform/plugins/shared/visualizations/public/utils/to_table_list_view_saved_object.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; -import type { - VisualizationListItem, - VisualizationStage, -} from '../vis_types/vis_type_alias_registry'; - -export type VisualizeUserContent = VisualizationListItem & - UserContentCommonSchema & { - type: string; - attributes: { - id: string; - title: string; - description?: string; - readOnly: boolean; - error?: string; - }; - }; - -export const toTableListViewSavedObject = ( - savedObject: Record -): VisualizeUserContent => { - return { - id: savedObject.id as string, - updatedAt: savedObject.updatedAt as string, - managed: savedObject.managed as boolean, - references: savedObject.references as Array<{ id: string; type: string; name: string }>, - type: savedObject.savedObjectType as string, - icon: savedObject.icon as string, - stage: savedObject.stage as VisualizationStage, - savedObjectType: savedObject.savedObjectType as string, - typeTitle: savedObject.typeTitle as string, - title: (savedObject.title as string) ?? '', - error: (savedObject.error as string) ?? '', - editor: savedObject.editor as any, - attributes: { - id: savedObject.id as string, - title: (savedObject.title as string) ?? '', - description: savedObject.description as string, - readOnly: savedObject.readOnly as boolean, - error: savedObject.error as string, - }, - }; -}; diff --git a/src/platform/plugins/shared/visualizations/public/visualization_listing/find_visualizations.ts b/src/platform/plugins/shared/visualizations/public/visualization_listing/find_visualizations.ts new file mode 100644 index 0000000000000..aae9ba5f60ac2 --- /dev/null +++ b/src/platform/plugins/shared/visualizations/public/visualization_listing/find_visualizations.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Reference } from '@kbn/content-management-utils'; +import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; +import type { VisualizationListItem } from '../vis_types/vis_type_alias_registry'; +import type { VisualizationsTabServices } from '.'; + +export type VisualizeUserContent = VisualizationListItem & + UserContentCommonSchema & { + type: string; + attributes: { + id: string; + title: string; + description?: string; + readOnly: boolean; + error?: string; + }; + }; + +export async function findVisualizationsForDashboard( + searchTerm: string, + services: VisualizationsTabServices, + options?: { references?: Reference[]; referencesToExclude?: Reference[] } +): Promise<{ total: number; hits: VisualizeUserContent[] }> { + const { core, visualizationsService } = services; + const limit = core.uiSettings.get('savedObjects:listingLimit'); + + const { total, hits } = await visualizationsService.findListItems( + searchTerm, + limit, + options?.references, + options?.referencesToExclude + ); + + const mappedHits = hits.map((item) => { + const vis = item as VisualizationListItem & { readOnly?: boolean }; + return { + ...vis, + type: vis.type || vis.savedObjectType, + attributes: { + id: vis.id, + title: vis.title, + description: vis.description, + readOnly: vis.readOnly ?? false, + error: vis.error, + }, + }; + }) as VisualizeUserContent[]; + + return { + total, + hits: mappedHits, + }; +} diff --git a/src/platform/plugins/shared/visualizations/public/visualization_listing/get_table_list.tsx b/src/platform/plugins/shared/visualizations/public/visualization_listing/get_table_list.tsx new file mode 100644 index 0000000000000..f36fed1e93905 --- /dev/null +++ b/src/platform/plugins/shared/visualizations/public/visualization_listing/get_table_list.tsx @@ -0,0 +1,314 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useMemo, useRef } from 'react'; +import { css } from '@emotion/react'; +import { logicalSizeCSS, useEuiTheme } from '@elastic/eui'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative } from '@kbn/i18n-react'; +import type { Reference } from '@kbn/content-management-utils'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { OpenContentEditorParams } from '@kbn/content-management-content-editor'; +import { + TableListViewTable, + TableListViewKibanaProvider, +} from '@kbn/content-management-table-list-view-table'; +import type { TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { EmbeddableStart, EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; +import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { findVisualizationsForDashboard, type VisualizeUserContent } from './find_visualizations'; +import { getVisualizationListingColumn } from './visualization_listing_helpers'; +import { getVisualizationListingEmptyPrompt } from './visualization_listing_helpers'; +import { + deleteListItems, + updateBasicSoAttributes, +} from '../utils/saved_objects_utils/update_basic_attributes'; +import { checkForDuplicateTitle } from '../utils/saved_objects_utils/check_for_duplicate_title'; +import type { TypesStart } from '../vis_types'; + +export interface VisualizationsTabServices { + core: CoreStart; + embeddable: EmbeddableStart; + savedObjectsTagging?: SavedObjectsTaggingApi; + contentManagement: ContentManagementPublicStart; + visualizeCapabilities: Record>; + showNewVisModal: () => () => void; + navigateToVisualization: (stateTransfer: EmbeddableStateTransfer, id: string) => void; + visualizationsService: { + findListItems: ( + searchTerm: string, + limit: number, + references?: Reference[], + referencesToExclude?: Reference[] + ) => ReturnType; + getTypes: () => TypesStart; + }; +} + +// Internal component that uses hooks +const VisualizationsTableListContent = ({ + parentProps, + services, +}: { + parentProps: TableListTabParentProps; + services: VisualizationsTabServices; +}) => { + const { + core, + embeddable, + savedObjectsTagging, + contentManagement, + visualizeCapabilities, + showNewVisModal, + navigateToVisualization, + visualizationsService, + } = services; + + // Store current visualizations for reference in content editor + const visualizedUserContent = useRef(); + + // Create visualization handler + const createItem = (): void => { + showNewVisModal(); + }; + + // Content editor save handler - updates title, description, tags + const onContentEditorSave = useCallback( + async (args: { id: string; title: string; description?: string; tags: string[] }) => { + const content = visualizedUserContent.current?.find(({ id }) => id === args.id); + + if (content) { + await updateBasicSoAttributes( + content.attributes.id, + content.type, + { + title: args.title, + description: args.description ?? '', + tags: args.tags, + }, + { + savedObjectsTagging, + overlays: core.overlays, + typesService: visualizationsService.getTypes(), + contentManagement, + http: core.http, + } + ); + } + }, + [core, savedObjectsTagging, contentManagement, visualizationsService] + ); + + // Content editor validators - check for duplicate titles + const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( + () => ({ + title: [ + { + type: 'warning', + async fn(value, id) { + if (id) { + const content = visualizedUserContent.current?.find((c) => c.id === id); + if (content) { + try { + await checkForDuplicateTitle( + { + id, + title: value, + lastSavedTitle: content.title, + }, + false, + false, + () => {} + ); + } catch (e) { + return i18n.translate('visualizations.listing.duplicateTitleWarning', { + defaultMessage: 'Saving "{value}" creates a duplicate title.', + values: { + value, + }, + }); + } + } + } + }, + }, + ], + }), + [] + ); + + // Edit item handler - used by actions column + const editItem = async (item: VisualizeUserContent) => { + const { id } = item.attributes; + const { editor } = item; + + // Handle custom editor (e.g., onEdit callback) + if (editor && 'onEdit' in editor) { + await editor.onEdit(id); + return; + } + + // Use editor config if available (e.g., maps have editApp='maps') + if (editor && 'editUrl' in editor && editor.editApp) { + core.application.navigateToApp(editor.editApp, { path: editor.editUrl }); + return; + } + + // Default: open in visualize app + navigateToVisualization(embeddable.getStateTransfer(), id!); + }; + + // Get visualization capabilities + const canSave = (visualizeCapabilities?.save as boolean) ?? false; + const canDelete = + (visualizeCapabilities?.delete as boolean) ?? (visualizeCapabilities?.save as boolean) ?? false; + + // Delete items handler + const deleteItemsHandler = useCallback( + async (items: object[]) => { + await deleteListItems(items, { + savedObjectsTagging, + overlays: core.overlays, + typesService: visualizationsService.getTypes(), + contentManagement, + http: core.http, + }).catch((error) => { + core.notifications.toasts.addError(error, { + title: i18n.translate('visualizations.listing.deleteErrorTitle', { + defaultMessage: 'Error deleting visualization', + }), + }); + }); + }, + [core, savedObjectsTagging, contentManagement, visualizationsService] + ); + + // Conditionally provide deleteItems based on capabilities + const deleteItems = canDelete ? deleteItemsHandler : undefined; + + // Click handler for visualization items (title click) + const getOnClickTitle = (item: VisualizeUserContent) => { + // Don't allow clicking on read-only visualizations + if (item.attributes.readOnly || item.error) { + return undefined; + } + + return () => editItem(item); + }; + + // Row item actions - control action button states + const rowItemActions = useCallback( + (item: VisualizeUserContent) => { + const { managed } = item; + const isReadOnlyVisualization = item.attributes.readOnly; + + // Disable actions for items without save permissions, managed items, or read-only visualizations + if (!canSave || managed || isReadOnlyVisualization) { + return { + edit: { + enabled: false, + reason: managed + ? i18n.translate('visualizations.listing.managedVisualizationMessage', { + defaultMessage: + 'Elastic manages this visualization. Changing it is not possible.', + }) + : isReadOnlyVisualization + ? i18n.translate('visualizations.listing.readOnlyVisualizationMessage', { + defaultMessage: + "These details can't be edited because this visualization is no longer supported.", + }) + : undefined, + }, + }; + } + + return undefined; + }, + [canSave] + ); + + // Find items handler - used for searching/filtering + const findItems = async ( + searchTerm: string, + options?: { references?: Reference[]; referencesToExclude?: Reference[] } + ) => { + const result = await findVisualizationsForDashboard(searchTerm, services, options); + // Store for content editor reference + visualizedUserContent.current = result.hits; + return result; + }; + + const { euiTheme } = useEuiTheme(); + + return ( + +
+ + tableCaption={i18n.translate('visualizations.listing.table.listTitle', { + defaultMessage: 'Visualizations', + })} + entityName={i18n.translate('visualizations.listing.table.entityName', { + defaultMessage: 'visualization', + })} + entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', { + defaultMessage: 'visualizations', + })} + id="visualizations" + initialPageSize={10} + findItems={findItems} + createItem={createItem} + editItem={editItem} + deleteItems={deleteItems} + contentEditor={{ + isReadonly: !canSave, + onSave: onContentEditorSave, + customValidators: contentEditorValidators, + }} + emptyPrompt={getVisualizationListingEmptyPrompt(createItem)} + getOnClickTitle={getOnClickTitle} + rowItemActions={rowItemActions} + customTableColumn={ + getVisualizationListingColumn() as EuiBasicTableColumn + } + {...parentProps} + /> +
+
+ ); +}; + +// Main export: regular function (not a component) that returns JSX +// This is called from async context, so it cannot use hooks directly +export const GetVisualizationsTableList = ( + parentProps: TableListTabParentProps, + services: VisualizationsTabServices +) => { + return ; +}; diff --git a/src/platform/plugins/shared/visualizations/public/visualization_listing/index.ts b/src/platform/plugins/shared/visualizations/public/visualization_listing/index.ts new file mode 100644 index 0000000000000..0753055317a5c --- /dev/null +++ b/src/platform/plugins/shared/visualizations/public/visualization_listing/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { GetVisualizationsTableList, type VisualizationsTabServices } from './get_table_list'; +export type { VisualizeUserContent } from './find_visualizations'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/utils/visualization_listing_helpers.tsx b/src/platform/plugins/shared/visualizations/public/visualization_listing/visualization_listing_helpers.tsx similarity index 72% rename from src/platform/plugins/shared/dashboard/public/dashboard_listing/utils/visualization_listing_helpers.tsx rename to src/platform/plugins/shared/visualizations/public/visualization_listing/visualization_listing_helpers.tsx index f01e38bfcfdef..771e60b94028a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/utils/visualization_listing_helpers.tsx +++ b/src/platform/plugins/shared/visualizations/public/visualization_listing/visualization_listing_helpers.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { VisualizationListItem } from '@kbn/visualizations-plugin/public'; +import type { VisualizationListItem } from '../vis_types/vis_type_alias_registry'; const getBadge = (item: VisualizationListItem) => { if (item.stage === 'beta') { @@ -19,10 +19,10 @@ const getBadge = (item: VisualizationListItem) => { { ); } @@ -70,7 +67,7 @@ const renderItemTypeIcon = (item: VisualizationListItem) => { export const getVisualizationListingColumn = () => { return { field: 'typeTitle', - name: i18n.translate('dashboard.weightedVisualizationsTab.listing.table.typeColumnName', { + name: i18n.translate('visualizations.listing.table.typeColumnName', { defaultMessage: 'Type', }), sortable: true, @@ -97,10 +94,7 @@ export const getVisualizationListingColumn = () => { type="warning" size="m" /> - + ); @@ -116,10 +110,7 @@ export const getVisualizationListingColumn = () => { type="error" size="m" /> - + ); @@ -133,7 +124,7 @@ export const getVisualizationListingEmptyPrompt = (createItem: () => void) => ( title={

@@ -141,7 +132,7 @@ export const getVisualizationListingEmptyPrompt = (createItem: () => void) => ( body={

@@ -149,7 +140,7 @@ export const getVisualizationListingEmptyPrompt = (createItem: () => void) => ( actions={ diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx index e11658ae5b540..7c8e4cab89b9f 100644 --- a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -45,10 +45,32 @@ import { getTypes } from '../../services'; import type { VisualizeServices } from '../types'; import { getNoItemsMessage, getCustomColumn, getCustomSortingOptions } from '../utils'; import { getVisualizeListItemLinkFn } from '../utils/get_visualize_list_item_link'; -import { - toTableListViewSavedObject, - type VisualizeUserContent, -} from '../../utils/to_table_list_view_saved_object'; +import type { VisualizeUserContent } from '../../visualization_listing/find_visualizations'; +import type { VisualizationStage } from '../../vis_types/vis_type_alias_registry'; + +const toTableListViewSavedObject = (savedObject: Record): VisualizeUserContent => { + return { + id: savedObject.id as string, + updatedAt: savedObject.updatedAt as string, + managed: savedObject.managed as boolean, + references: savedObject.references as Array<{ id: string; type: string; name: string }>, + type: savedObject.savedObjectType as string, + icon: savedObject.icon as string, + stage: savedObject.stage as VisualizationStage, + savedObjectType: savedObject.savedObjectType as string, + typeTitle: savedObject.typeTitle as string, + title: (savedObject.title as string) ?? '', + error: (savedObject.error as string) ?? '', + editor: savedObject.editor as any, + attributes: { + id: savedObject.id as string, + title: (savedObject.title as string) ?? '', + description: savedObject.description as string, + readOnly: savedObject.readOnly as boolean, + error: savedObject.error as string, + }, + }; +}; const visualizeListingStyles = { table: ({ euiTheme }: UseEuiTheme) => css` diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts index d74c13da33895..ff1259a09e53f 100644 --- a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts +++ b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts @@ -13,7 +13,7 @@ import { createHashHistory } from 'history'; import { FilterStateStore } from '@kbn/es-query'; import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { GLOBAL_STATE_STORAGE_KEY } from '@kbn/visualizations-common'; -import type { VisualizeUserContent } from '../../utils/to_table_list_view_saved_object'; +import type { VisualizeUserContent } from '../../visualization_listing/find_visualizations'; const mockItem: VisualizeUserContent = { id: '9886b410-4c8b-11e8-b3d7-01146121b73d', diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts index 4e5637b7e1166..8b5af101f3f42 100644 --- a/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts +++ b/src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.ts @@ -13,7 +13,7 @@ import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import { GLOBAL_STATE_STORAGE_KEY, VISUALIZE_APP_NAME } from '@kbn/visualizations-common'; import { getUISettings } from '../../services'; -import type { VisualizeUserContent } from '../../utils/to_table_list_view_saved_object'; +import type { VisualizeUserContent } from '../../visualization_listing/find_visualizations'; export const getVisualizeListItemLinkFn = (application: ApplicationStart, kbnUrlStateStorage: IKbnUrlStateStorage) => diff --git a/tsconfig.base.json b/tsconfig.base.json index 57e3b503ca56b..395db7e77f916 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2426,6 +2426,8 @@ "@kbn/vis-type-vislib-plugin/*": ["src/platform/plugins/private/vis_types/vislib/*"], "@kbn/vis-type-xy-plugin": ["src/platform/plugins/private/vis_types/xy"], "@kbn/vis-type-xy-plugin/*": ["src/platform/plugins/private/vis_types/xy/*"], + "@kbn/visualization-listing-plugin": ["src/platform/plugins/private/visualization_listing"], + "@kbn/visualization-listing-plugin/*": ["src/platform/plugins/private/visualization_listing/*"], "@kbn/visualization-ui-components": ["src/platform/packages/shared/kbn-visualization-ui-components"], "@kbn/visualization-ui-components/*": ["src/platform/packages/shared/kbn-visualization-ui-components/*"], "@kbn/visualization-utils": ["src/platform/packages/shared/kbn-visualization-utils"],