diff --git a/package.json b/package.json index 56b31931aa1a0..11a2eba7b18ea 100644 --- a/package.json +++ b/package.json @@ -239,6 +239,7 @@ "@kbn/content-management-content-editor": "link:src/platform/packages/shared/content-management/content_editor", "@kbn/content-management-content-insights-public": "link:src/platform/packages/shared/content-management/content_insights/content_insights_public", "@kbn/content-management-content-insights-server": "link:src/platform/packages/shared/content-management/content_insights/content_insights_server", + "@kbn/content-management-content-source": "link:src/platform/packages/shared/content-management/content_source", "@kbn/content-management-examples-plugin": "link:examples/content_management_examples", "@kbn/content-management-favorites-common": "link:src/platform/packages/shared/content-management/favorites/favorites_common", "@kbn/content-management-favorites-public": "link:src/platform/packages/shared/content-management/favorites/favorites_public", diff --git a/src/platform/packages/shared/content-management/content_source/README.md b/src/platform/packages/shared/content-management/content_source/README.md new file mode 100644 index 0000000000000..21bade06a930d --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/README.md @@ -0,0 +1,3 @@ +# @kbn/content-management-content-source + +A component that opens a flyout containing the source of a content item (such as a dashboard). diff --git a/src/platform/packages/shared/content-management/content_source/index.ts b/src/platform/packages/shared/content-management/content_source/index.ts new file mode 100644 index 0000000000000..1808a0e36a915 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/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 { ContentSourceKibanaProvider, useOpenContentSource } from './src'; +export type { ContentSourceFlyoutProps } from './src'; diff --git a/src/platform/packages/shared/content-management/content_source/jest.config.js b/src/platform/packages/shared/content-management/content_source/jest.config.js new file mode 100644 index 0000000000000..d424c415df0cc --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../..', + roots: ['/src/platform/packages/shared/content-management/content_source'], +}; diff --git a/src/platform/packages/shared/content-management/content_source/kibana.jsonc b/src/platform/packages/shared/content-management/content_source/kibana.jsonc new file mode 100644 index 0000000000000..ee23ae6e78daa --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/content-management-content-source", + "owner": "@elastic/kibana-presentation", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/content-management/content_source/package.json b/src/platform/packages/shared/content-management/content_source/package.json new file mode 100644 index 0000000000000..ef192e1f72c03 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/content-management-content-source", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/content-management/content_source/src/components/content_flyout.tsx b/src/platform/packages/shared/content-management/content_source/src/components/content_flyout.tsx new file mode 100644 index 0000000000000..3fab97d453865 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/components/content_flyout.tsx @@ -0,0 +1,166 @@ +/* + * 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, useEffect, useState } from 'react'; +import { css } from '@emotion/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiLoadingSpinner, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +// @ts-expect-error untyped library +import { saveAs } from '@elastic/filesaver'; + +const flyoutBodyCss = css` + height: 100%; + .euiFlyoutBody__overflowContent { + height: 100%; + padding: 0; + } +`; + +export interface ContentSourceFlyoutProps { + onClose: () => void; + getContent: () => Promise; + contentName: string; +} + +export const ContentSourceFlyout: React.FC = ({ + onClose, + getContent, + contentName, +}) => { + const [{ content, loading, error }, setContent] = useState<{ + loading: boolean; + content: Awaited | null; + error: unknown | null; + }>({ loading: true, content: null, error: null }); + + const onDownload = useCallback(() => { + const blob = new Blob([JSON.stringify(content, null, 2)], { + type: 'application/json', + }); + saveAs(blob, 'export.json'); + }, [content]); + + useEffect(() => { + const loadContent = () => { + getContent() + .then((_content) => + setContent((prevState) => ({ + ...prevState, + loading: false, + content: _content, + })) + ) + .catch((err) => + setContent((prevState) => ({ + ...prevState, + loading: false, + error: err, + })) + ); + }; + + loadContent(); + }, [getContent]); + + return ( + <> + + +

+ +

+
+
+ + {error ? ( + + } + color="danger" + iconType="alert" + /> + ) : ( + <> + {loading ? ( + } + /> + ) : ( + + {JSON.stringify(content, null, 2)} + + )} + + )} + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/platform/packages/shared/content-management/content_source/src/components/content_flyout_container.tsx b/src/platform/packages/shared/content-management/content_source/src/components/content_flyout_container.tsx new file mode 100644 index 0000000000000..de738bbe7373f --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/components/content_flyout_container.tsx @@ -0,0 +1,20 @@ +/* + * 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 from 'react'; +import type { FC } from 'react'; + +import { ContentSourceFlyout } from './content_flyout'; +import type { ContentSourceFlyoutProps } from './content_flyout'; + +export type ContentSourceFlyoutContainerProps = ContentSourceFlyoutProps; + +export const ContentSourceFlyoutContainer: FC = (props) => { + return ; +}; diff --git a/src/platform/packages/shared/content-management/content_source/src/components/content_source_loader.tsx b/src/platform/packages/shared/content-management/content_source/src/components/content_source_loader.tsx new file mode 100644 index 0000000000000..284a6fd69af04 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/components/content_source_loader.tsx @@ -0,0 +1,36 @@ +/* + * 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 from 'react'; +import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader } from '@elastic/eui'; +import type { ContentSourceFlyoutContainerProps } from './content_flyout_container'; + +const ContentSourceFlyoutContentContainer = React.lazy(() => + import('./content_flyout_container').then( + ({ ContentSourceFlyoutContainer: _ContentSourceFlyoutContentContainer }) => ({ + default: _ContentSourceFlyoutContentContainer, + }) + ) +); + +export const ContentSourceLoader: React.FC = (props) => { + return ( + + + + + + } + > + + + ); +}; diff --git a/src/platform/packages/shared/content-management/content_source/src/components/index.ts b/src/platform/packages/shared/content-management/content_source/src/components/index.ts new file mode 100644 index 0000000000000..716043d92a7e5 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/components/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 { ContentSourceLoader } from './content_source_loader'; +export type { ContentSourceFlyoutProps } from './content_flyout'; diff --git a/src/platform/packages/shared/content-management/content_source/src/index.ts b/src/platform/packages/shared/content-management/content_source/src/index.ts new file mode 100644 index 0000000000000..b0c634a12f323 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { ContentSourceKibanaProvider } from './services'; +export { useOpenContentSource } from './open_content_source'; +export type { ContentSourceFlyoutProps } from './components'; diff --git a/src/platform/packages/shared/content-management/content_source/src/open_content_source.tsx b/src/platform/packages/shared/content-management/content_source/src/open_content_source.tsx new file mode 100644 index 0000000000000..ea097d223c944 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/open_content_source.tsx @@ -0,0 +1,40 @@ +/* + * 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, useRef } from 'react'; +import type { OverlayRef } from '@kbn/core-mount-utils-browser'; + +import type { ContentSourceFlyoutProps } from './components'; + +import { ContentSourceLoader } from './components'; +import { useServices } from './services'; + +export function useOpenContentSource() { + const services = useServices(); + const { openFlyout } = services; + const flyout = useRef(null); + + return useCallback( + (args: ContentSourceFlyoutProps) => { + const closeFlyout = () => { + flyout.current?.close(); + }; + + flyout.current = openFlyout(, { + maxWidth: 600, + size: 'm', + ownFocus: true, + hideCloseButton: true, + }); + + return closeFlyout; + }, + [openFlyout] + ); +} diff --git a/src/platform/packages/shared/content-management/content_source/src/services.tsx b/src/platform/packages/shared/content-management/content_source/src/services.tsx new file mode 100644 index 0000000000000..596b87152c693 --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/src/services.tsx @@ -0,0 +1,116 @@ +/* + * 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 { FC, PropsWithChildren, ReactNode } from 'react'; +import React, { useCallback, useContext } from 'react'; + +import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; +import type { I18nStart } from '@kbn/core-i18n-browser'; +import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser'; +import type { OverlayFlyoutOpenOptions } from '@kbn/core-overlays-browser'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; +import type { UserProfileService } from '@kbn/core-user-profile-browser'; +import { toMountPoint } from '@kbn/react-kibana-mount'; + +type NotifyFn = (title: JSX.Element, text?: string) => void; + +export interface Theme { + readonly darkMode: boolean; +} + +/** + * Abstract external services for this component. + */ +export interface Services { + openFlyout(node: ReactNode, options?: OverlayFlyoutOpenOptions): OverlayRef; + notifyError: NotifyFn; +} + +const ContentSourceContext = React.createContext(null); + +/** + * Abstract external service Provider. + */ +export const ContentSourceProvider: FC> = ({ + children, + ...services +}) => { + return {children}; +}; + +/** + * Specific services for mounting React + */ +interface ContentSourceStartServices { + analytics: Pick; + i18n: I18nStart; + theme: Pick; + userProfile: UserProfileService; +} + +/** + * Kibana-specific service types. + */ +export interface ContentSourceKibanaDependencies { + /** CoreStart contract */ + core: ContentSourceStartServices & { + overlays: { + openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; + }; + notifications: { + toasts: { + addDanger: (notifyArgs: { title: MountPoint; text?: string }) => void; + }; + }; + }; +} + +/** + * Kibana-specific Provider that maps to known dependency types. + */ +export const ContentSourceKibanaProvider: FC< + PropsWithChildren +> = ({ children, ...services }) => { + const { core } = services; + const { overlays, notifications, ...startServices } = core; + const { openFlyout: coreOpenFlyout } = overlays; + + const openFlyout = useCallback( + (node: ReactNode, options: OverlayFlyoutOpenOptions) => { + return coreOpenFlyout(toMountPoint(node, startServices), options); + }, + [coreOpenFlyout, startServices] + ); + + return ( + { + notifications.toasts.addDanger({ title: toMountPoint(title, startServices), text }); + }} + > + {children} + + ); +}; + +/** + * React hook for accessing pre-wired services. + */ +export function useServices() { + const context = useContext(ContentSourceContext); + + if (!context) { + throw new Error( + 'ContentSourceContext is missing. Ensure your component or React root is wrapped with or .' + ); + } + + return context; +} diff --git a/src/platform/packages/shared/content-management/content_source/tsconfig.json b/src/platform/packages/shared/content-management/content_source/tsconfig.json new file mode 100644 index 0000000000000..63f0b5ff33faa --- /dev/null +++ b/src/platform/packages/shared/content-management/content_source/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [] +} diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 0ddc8614db318..11a98ada89eff 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -12,8 +12,10 @@ import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react' import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import useMountedState from 'react-use/lib/useMountedState'; +import { useOpenContentSource } from '@kbn/content-management-content-source'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { UI_SETTINGS } from '../../../common'; +import { CONTENT_ID } from '../../../common/content_management'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays'; @@ -40,6 +42,7 @@ export const useDashboardMenuItems = ({ const [isSaveInProgress, setIsSaveInProgress] = useState(false); const dashboardApi = useDashboardApi(); + const openContentSource = useOpenContentSource(); const [dashboardTitle, hasOverlays, hasUnsavedChanges, lastSavedId, viewMode] = useBatchedPublishingSubjects( @@ -67,6 +70,17 @@ export const useDashboardMenuItems = ({ [dashboardTitle, hasUnsavedChanges, lastSavedId, dashboardApi] ); + /** + * Show Export flyout example + */ + const showExport = useCallback(() => { + const close = openContentSource({ + getContent: async () => dashboardApi.getSerializedState(), + onClose: () => close(), + contentName: CONTENT_ID, + }); + }, [dashboardApi, openContentSource]); + /** * Save the dashboard without any UI or popups. */ @@ -195,6 +209,15 @@ export const useDashboardMenuItems = ({ run: showShare, } as TopNavMenuData, + export: { + label: 'Export', + description: 'Export dashboard', + id: 'export', + testId: 'dashboardExportMenuItem', + disableButton: disableTopNav, + run: showExport, + }, + settings: { ...topNavStrings.settings, id: 'settings', @@ -210,6 +233,7 @@ export const useDashboardMenuItems = ({ lastSavedId, dashboardInteractiveSave, viewMode, + showExport, showShare, dashboardApi, setIsLabsShown, @@ -282,7 +306,13 @@ export const useDashboardMenuItems = ({ } else { editModeItems.push(menuItems.switchToViewMode, menuItems.interactiveSave); } - return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems]; + return [ + ...labsMenuItem, + menuItems.settings, + ...shareMenuItem, + menuItems.export, + ...editModeItems, + ]; }, [isLabsEnabled, menuItems, lastSavedId, showResetChange, resetChangesMenuItem]); return { viewModeTopNavConfig, editModeTopNavConfig }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx index 751f7f1d5e0ef..3afff269693eb 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/dashboard_top_nav_with_context.tsx @@ -8,10 +8,12 @@ */ import React from 'react'; +import { ContentSourceKibanaProvider } from '@kbn/content-management-content-source'; import { InternalDashboardTopNav, InternalDashboardTopNavProps, } from './internal_dashboard_top_nav'; +import { coreServices } from '../services/kibana_services'; import { DashboardContext } from '../dashboard_api/use_dashboard_api'; import { DashboardApi } from '../dashboard_api/types'; export interface DashboardTopNavProps extends InternalDashboardTopNavProps { @@ -20,7 +22,9 @@ export interface DashboardTopNavProps extends InternalDashboardTopNavProps { export const DashboardTopNavWithContext = (props: DashboardTopNavProps) => ( - + + + ); diff --git a/src/platform/plugins/shared/dashboard/tsconfig.json b/src/platform/plugins/shared/dashboard/tsconfig.json index 7f2c5c3a85b61..2e4ddbd377251 100644 --- a/src/platform/plugins/shared/dashboard/tsconfig.json +++ b/src/platform/plugins/shared/dashboard/tsconfig.json @@ -55,6 +55,7 @@ "@kbn/content-management-table-list-view-table", "@kbn/shared-ux-prompt-not-found", "@kbn/content-management-content-editor", + "@kbn/content-management-content-source", "@kbn/serverless", "@kbn/no-data-page-plugin", "@kbn/react-kibana-mount", diff --git a/tsconfig.base.json b/tsconfig.base.json index f76712c1adf60..a6dfa0426a452 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -208,6 +208,8 @@ "@kbn/content-management-content-insights-public/*": ["src/platform/packages/shared/content-management/content_insights/content_insights_public/*"], "@kbn/content-management-content-insights-server": ["src/platform/packages/shared/content-management/content_insights/content_insights_server"], "@kbn/content-management-content-insights-server/*": ["src/platform/packages/shared/content-management/content_insights/content_insights_server/*"], + "@kbn/content-management-content-source": ["src/platform/packages/shared/content-management/content_source"], + "@kbn/content-management-content-source/*": ["src/platform/packages/shared/content-management/content_source/*"], "@kbn/content-management-examples-plugin": ["examples/content_management_examples"], "@kbn/content-management-examples-plugin/*": ["examples/content_management_examples/*"], "@kbn/content-management-favorites-common": ["src/platform/packages/shared/content-management/favorites/favorites_common"], diff --git a/yarn.lock b/yarn.lock index aad975d6961d6..fb97ce2909ab4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4094,6 +4094,10 @@ version "0.0.0" uid "" +"@kbn/content-management-content-source@link:src/platform/packages/shared/content-management/content_source": + version "0.0.0" + uid "" + "@kbn/content-management-examples-plugin@link:examples/content_management_examples": version "0.0.0" uid ""