Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface TableListBreadcrumb {
export type TableListTabParentProps<T extends UserContentCommonSchema = UserContentCommonSchema> =
Pick<TableListViewTableProps<T>, 'onFetchSuccess' | 'setPageDataTestSubject'> & {
getBreadcrumbs?: (appId: string) => TableListBreadcrumb[];
showCreateButton?: boolean;
};

export interface TableListTab<T extends UserContentCommonSchema = UserContentCommonSchema> {
Expand All @@ -33,12 +34,14 @@ export interface TableListTab<T extends UserContentCommonSchema = UserContentCom

type TabbedTableListViewProps = Pick<
TableListViewProps<UserContentCommonSchema>,
'title' | 'description' | 'headingId' | 'children'
'description' | 'headingId' | 'children'
> & {
title?: TableListViewProps<UserContentCommonSchema>['title'];
tabs: TableListTab[];
activeTabId: string;
changeActiveTab: (id: string) => void;
getBreadcrumbs?: TableListTabParentProps['getBreadcrumbs'];
showCreateButton?: boolean;
};

export const TabbedTableListView = ({
Expand All @@ -50,6 +53,7 @@ export const TabbedTableListView = ({
activeTabId,
changeActiveTab,
getBreadcrumbs,
showCreateButton,
}: TabbedTableListViewProps) => {
const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false);
const [pageDataTestSubject, setPageDataTestSubject] = useState<string>();
Expand All @@ -71,17 +75,18 @@ export const TabbedTableListView = ({
onFetchSuccess,
setPageDataTestSubject,
getBreadcrumbs,
showCreateButton,
});
setTableList(newTableList);
}

loadTableList();
}, [activeTabId, tabs, getActiveTab, onFetchSuccess, getBreadcrumbs]);
}, [activeTabId, tabs, getActiveTab, onFetchSuccess, getBreadcrumbs, showCreateButton]);

return (
<KibanaPageTemplate panelled data-test-subj={pageDataTestSubject}>
<KibanaPageTemplate.Header
pageTitle={<span id={headingId}>{title}</span>}
pageTitle={title ? <span id={headingId}>{title}</span> : undefined}
description={description}
data-test-subj="top-nav"
tabs={tabs.map((tab) => ({
Expand All @@ -90,7 +95,9 @@ export const TabbedTableListView = ({
label: tab.title,
}))}
/>
<KibanaPageTemplate.Section aria-labelledby={hasInitialFetchReturned ? headingId : undefined}>
<KibanaPageTemplate.Section
aria-labelledby={hasInitialFetchReturned && title ? headingId : undefined}
>
{/* Any children passed to the component */}
{children}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import type { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import { firstValueFrom } from 'rxjs';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
Expand Down Expand Up @@ -101,7 +102,35 @@ export class EventAnnotationListingPlugin
},
};
dependencies.visualizations.listingViewRegistry.add(annotationGroupsTabConfig);
dependencies.dashboard.registerListingPageTab(annotationGroupsTabConfig);
dependencies.dashboard.registerListingPageTab({
...annotationGroupsTabConfig,
createAction: async () => {
const [coreStart, pluginsStart] = await core.getStartServices();
const currentApp = await firstValueFrom(coreStart.application.currentAppId$);
if (!currentApp) return;
const stateTransfer = pluginsStart.embeddable.getStateTransfer();
const breadcrumbs = [
{
text: stateTransfer.getAppNameFromId(currentApp) ?? currentApp,
href: coreStart.application.getUrlForApp(currentApp),
},
{
text: tabTitle,
href: coreStart.application.getUrlForApp(currentApp, {
path: window.location.hash,
}),
},
];
await stateTransfer.navigateToEditor('lens', {
path: '',
state: {
originatingApp: currentApp,
originatingPath: window.location.hash,
breadcrumbs,
},
});
},
});
}

public start(core: CoreStart, plugins: object): void {
Expand Down
1 change: 1 addition & 0 deletions src/platform/plugins/shared/dashboard/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dependsOn:
- '@kbn/dashboard-navigation-options-components'
- '@kbn/dashboard-navigation-options-schema'
- '@kbn/as-code-shared-telemetry'
- '@kbn/app-header'
tags:
- plugin
- prod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jest.mock('@kbn/content-management-tabbed-table-list-view', () => ({
TabbedTableListView: jest.fn().mockReturnValue(null),
}));

jest.mock('@kbn/app-header', () => ({
__esModule: true,
Comment thread
Dosant marked this conversation as resolved.
AppHeader: () => null,
}));

const renderDashboardListing = (
props: Partial<DashboardListingProps> = {},
{ initialEntries = ['/list'] }: { initialEntries?: string[] } = {}
Expand Down Expand Up @@ -58,7 +63,6 @@ test('renders TabbedTableListView with correct title and dashboards tab', () =>
expect(mockTabbedTableListView).toHaveBeenCalledTimes(1);
const props = mockTabbedTableListView.mock.calls[0][0];
expect(props).toMatchObject({
title: 'Dashboards',
headingId: 'dashboardListingHeading',
});
expect(props.tabs[0]).toMatchObject({ id: 'dashboards', title: 'Dashboards' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { QueryClientProvider } from '@kbn/react-query';
import type { EmbeddableEditorBreadcrumb } from '@kbn/embeddable-plugin/public';

import { AppHeader } from '@kbn/app-header';
import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components';
import { coreServices } from '../services/kibana_services';
import { dashboardQueryClient } from '../services/dashboard_query_client';
import { DASHBOARD_APP_ID, LANDING_PAGE_PATH } from '../../common/page_bundle_constants';
import { getDashboardListingTabs } from './get_dashboard_listing_tabs';
import type { DashboardListingProps } from './types';
import type { DashboardListingProps, DashboardListingTab } from './types';

export const DashboardListing = ({
children,
Expand Down Expand Up @@ -86,19 +88,64 @@ export const DashboardListing = ({
[tabs, activeTabId]
);

const appMenu: AppMenuConfig = useMemo(() => {
const tabsByIdMap = new Map((tabs as DashboardListingTab[]).map((tab) => [tab.id, tab]));
return {
primaryActionItem: {
id: 'create',
testId: 'dashboardListingCreateButton',
iconType: 'plus',
label: i18n.translate('dashboard.listing.createButtonLabel', {
defaultMessage: 'Create',
}),
popoverWidth: 180,
items: [
{
id: 'createDashboard',
order: 1,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Hehe, I still think it is weird that the API requires order for the array of items :D
The array is the natural order in this case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't always be the case that you will define all items in one file and having an if branch "if there's order, use it if not then use the order items were passed in" seems confusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what an example of this is and what it looks like in code.

I understand order for registry like api - registerItem({}) when you register from multiple places. There it makes sense.

But when it is a place where we pass the final order into the component, this seems confusing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look at how discover defines app menu - each button is defined in a separate file. It's easier to understand which button has which position when you see order in that case. Also discover has a functionality built on-top of app menu (discover profiles) that lets you register custom app menu buttons, which is even more of a reason to keep order.

label: 'Dashboard',
iconType: 'productDashboard',
testId: 'createDashboardButton',
run: () => tabsByIdMap.get('dashboards')?.createAction?.(),
},
{
id: 'createVisualization',
order: 2,
label: 'Visualization',
iconType: 'chartBarVertical',
testId: 'createVisualizationButton',
run: () => tabsByIdMap.get('visualizations')?.createAction?.(),
},
{
id: 'createAnnotation',
order: 3,
label: 'Annotation',
iconType: 'flag',
testId: 'createAnnotationButton',
run: () => tabsByIdMap.get('annotations')?.createAction?.(),
},
],
},
};
}, [tabs]);

return (
<I18nProvider>
<QueryClientProvider client={dashboardQueryClient}>
{children}
<TabbedTableListView
headingId="dashboardListingHeading"
<AppHeader
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Curious why we aren't upgrading the header inside the TabbedTableListView

Copy link
Copy Markdown
Member Author

@kowalczyk-krzysztof kowalczyk-krzysztof Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason really. I picked the first component that had tabs defined already. We could move it one level down or even up but I think it's best for presentation team to decide on that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought there was a good reason. because otherwise more 1-to-1 migration would be putting it into TabbedTableListView where PageTemplate wraps the header, so it would be my default choice

title={i18n.translate('dashboard.listing.title', {
defaultMessage: 'Dashboards',
})}
menu={appMenu}
/>
<TabbedTableListView
headingId="dashboardListingHeading"
getBreadcrumbs={getBreadcrumbs}
tabs={tabs}
activeTabId={activeTabId}
changeActiveTab={changeActiveTab}
showCreateButton={false}
/>
</QueryClientProvider>
</I18nProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@

import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type {
TableListTab,
TableListTabParentProps,
} from '@kbn/content-management-tabbed-table-list-view';
import type { TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view';
import {
TableListViewTable,
TableListViewKibanaProvider,
Expand All @@ -29,7 +26,13 @@ import {
} from '../services/kibana_services';
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
import { useDashboardListingTable } from './hooks/use_dashboard_listing_table';
import type { DashboardListingProps, DashboardSavedObjectUserContent } from './types';
import { confirmCreateWithUnsaved } from './confirm_overlays';
import { getDashboardBackupService } from '../services/dashboard_api_services';
import type {
DashboardListingProps,
DashboardListingTab,
DashboardSavedObjectUserContent,
} from './types';

type GetDashboardListingTabsParams = Pick<
DashboardListingProps,
Expand Down Expand Up @@ -64,6 +67,7 @@ const DashboardsTabContent = ({
getDashboardUrl,
useSessionStorageIntegration,
initialFilter,
showCreateDashboardButton: parentProps.showCreateButton,
});

const dashboardFavoritesClient = useMemo(() => {
Expand Down Expand Up @@ -101,22 +105,32 @@ export const getDashboardListingTabs = ({
useSessionStorageIntegration,
initialFilter,
getTabs,
}: GetDashboardListingTabsParams): TableListTab<DashboardSavedObjectUserContent>[] => {
}: GetDashboardListingTabsParams): DashboardListingTab[] => {
const commonProps = {
goToDashboard,
getDashboardUrl,
useSessionStorageIntegration,
initialFilter,
};

const dashboardsTab: TableListTab<DashboardSavedObjectUserContent> = {
const dashboardsTab: DashboardListingTab = {
title: i18n.translate('dashboard.listing.tabs.dashboards.title', {
defaultMessage: 'Dashboards',
}),
id: 'dashboards',
getTableList: (parentProps) => (
<DashboardsTabContent {...commonProps} parentProps={parentProps} />
),
createAction: () => {
if (useSessionStorageIntegration && getDashboardBackupService().dashboardHasUnsavedEdits()) {
confirmCreateWithUnsaved(() => {
getDashboardBackupService().clearState();
goToDashboard();
}, goToDashboard);
return;
}
goToDashboard();
},
};

// Additional tabs (e.g., visualizations and annotation groups)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import type { SavedObjectAccessControl } from '@kbn/core-saved-objects-common';
import type { TableListTab } from '@kbn/content-management-tabbed-table-list-view';
import type { AppDeepLinkLocations } from '@kbn/core/public';

/** Tab interface with optional deep link support. */
/** Tab interface with optional deep link and create action support. */
export type DashboardListingTab = TableListTab & {
deepLink?: {
/** Title to display in global search results */
title: string;
/** Where this deep link should be visible. */
visibleIn?: AppDeepLinkLocations[];
};
createAction?: () => void;
};

export type DashboardListingProps = PropsWithChildren<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ spaceTest.describe(
'clicking create new dashboard button navigates to the editor',
async () => {
await pageObjects.dashboard.goto();
await page.testSubj.click('newItemButton');
await page.testSubj.click('dashboardListingCreateButton');
await page.testSubj.click('createDashboardButton');
await expect(page.testSubj.locator('dashboardAddTopNavButton')).toBeVisible({
timeout: 20_000,
});
Expand All @@ -48,7 +49,7 @@ spaceTest.describe(

await spaceTest.step('navigating back to listing page from a new dashboard', async () => {
await page.goBack();
await expect(page.testSubj.locator('newItemButton')).toBeVisible();
await expect(page.testSubj.locator('dashboardListingCreateButton')).toBeVisible();
});
}
);
Expand All @@ -57,7 +58,8 @@ spaceTest.describe(
'saving a dashboard and returning to the listing page shows it',
async ({ page, pageObjects }) => {
await pageObjects.dashboard.goto();
await page.testSubj.click('newItemButton');
await page.testSubj.click('dashboardListingCreateButton');
await page.testSubj.click('createDashboardButton');
await expect(page.testSubj.locator('dashboardAddTopNavButton')).toBeVisible({
timeout: 20_000,
});
Expand Down
3 changes: 2 additions & 1 deletion src/platform/plugins/shared/dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@
"@kbn/dashboard-navigation-options-common",
"@kbn/dashboard-navigation-options-components",
"@kbn/dashboard-navigation-options-schema",
"@kbn/as-code-shared-telemetry"
"@kbn/as-code-shared-telemetry",
"@kbn/app-header"
],
"exclude": [
"target/**/*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const VisualizationTableList = ({
savedObjectsTagging,
parentProps,
}: VisualizationTableListProps) => {
const { getBreadcrumbs, onFetchSuccess, setPageDataTestSubject } = parentProps;
const { getBreadcrumbs, onFetchSuccess, setPageDataTestSubject, showCreateButton } = parentProps;
const euiThemeContext = useEuiTheme();
const tableStyles = useMemo(
() => getVisualizationListingTableStyles(euiThemeContext),
Expand All @@ -74,14 +74,15 @@ export const VisualizationTableList = ({
const visualizedUserContent = useRef<VisualizeUserContent[]>();
const closeNewVisModal = useRef(() => {});

const createNewVis = useCallback(async () => {
const currentApp = await firstValueFrom(core.application.currentAppId$);
const breadcrumbs = currentApp ? getBreadcrumbs?.(currentApp) : undefined;
closeNewVisModal.current = visualizations.showNewVisModal({
originatingApp: currentApp,
originatingPath: window.location.hash,
breadcrumbs,
outsideVisualizeApp: currentApp !== VISUALIZE_APP_NAME,
const createNewVis = useCallback(() => {
firstValueFrom(core.application.currentAppId$).then((currentApp) => {
const breadcrumbs = currentApp ? getBreadcrumbs?.(currentApp) : undefined;
closeNewVisModal.current = visualizations.showNewVisModal({
originatingApp: currentApp,
originatingPath: window.location.hash,
breadcrumbs,
outsideVisualizeApp: currentApp !== VISUALIZE_APP_NAME,
});
});
}, [visualizations, core.application, getBreadcrumbs]);

Expand Down Expand Up @@ -244,7 +245,7 @@ export const VisualizationTableList = ({
customValidators: contentEditorValidators,
}}
emptyPrompt={noItemsFragment}
createItem={createNewVis}
createItem={showCreateButton === false ? undefined : createNewVis}
customTableColumn={getCustomColumn()}
customSortingOptions={getCustomSortingOptions()}
initialPageSize={initialPageSize}
Expand Down
Loading
Loading