Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions examples/portable_dashboards_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -125,13 +128,14 @@ const DashboardsDemo = ({
};

const PortableDashboardListingDemo = ({ history }: { history: AppMountParameters['history'] }) => {
const listingViewRegistry: DashboardListingViewRegistry = new Set();
return (
<DashboardListingTable
goToDashboard={(dashboardId) =>
alert(`Here's where I would redirect you to ${dashboardId ?? 'a new Dashboard'}`)
}
getDashboardUrl={() => 'https://www.elastic.co/'}
listingViewRegistry={new Set()}
listingViewRegistry={listingViewRegistry}
>
<EuiButton onClick={() => history.push(DASHBOARD_DEMO_PATH)}>
Go back to usage demos
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,8 @@ pageLoadAssetSize:
visTypeVega: 38538
visTypeVislib: 14679
visTypeXy: 32342
visualizations: 53333
visualizationListing: 10000
visualizations: 38375
watcher: 10485
workflowsExtensions: 61264
workflowsManagement: 8497
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface EventAnnotationListingStartDependencies {

interface SetupDependencies {
visualizations: VisualizationsSetup;
dashboard: DashboardSetup;
dashboard?: DashboardSetup;
}

/** @public */
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions src/platform/plugins/private/visualization_listing/README.md
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions src/platform/plugins/private/visualization_listing/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -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": []
}
}
17 changes: 17 additions & 0 deletions src/platform/plugins/private/visualization_listing/public/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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<StartDependencies>, 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 {}
}
17 changes: 17 additions & 0 deletions src/platform/plugins/private/visualization_listing/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/**/*"]
}
7 changes: 3 additions & 4 deletions src/platform/plugins/shared/dashboard/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"dataViews",
"dataViewEditor",
"embeddable",
"eventAnnotation",
"fieldFormats",
"controls",
"inspector",
Expand All @@ -27,8 +26,7 @@
"uiActions",
"urlForwarding",
"presentationUtil",
"unifiedSearch",
"visualizations"
"unifiedSearch"
],
"optionalPlugins": [
"home",
Expand All @@ -42,7 +40,8 @@
"noDataPage",
"observabilityAIAssistant",
"lens",
"cps"
"cps",
"visualizations"
],
"requiredBundles": [
"kibanaReact",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -37,7 +37,7 @@ export const DashboardListingPage = ({
initialFilter,
kbnUrlStateStorage,
}: DashboardListingPageProps) => {
const { listingViewRegistry } = useContext(DashboardMountContext);
const { listingViewRegistry } = useDashboardMountContext();
const [showNoDataPage, setShowNoDataPage] = useState<boolean | undefined>();
useEffect(() => {
let isMounted = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ const renderDashboardListingEmptyPrompt = (props: Partial<DashboardListingEmptyP
<DashboardListingEmptyPrompt
createItem={jest.fn()}
goToDashboard={jest.fn()}
setUnsavedDashboardIds={jest.fn()}
refreshUnsavedDashboards={jest.fn()}
unsavedDashboardIds={[]}
useSessionStorageIntegration={true}
disableCreateDashboardButton={false}
{...props}
/>,
{ wrapper: I18nProvider }
Expand All @@ -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 () => {
Expand Down
Loading